Merge pull request #96 from microsoft/lramos15/shimTesting

Make the telemetry module testable
This commit is contained in:
Logan Ramos 2022-05-03 09:48:53 -04:00 коммит произвёл GitHub
Родитель 8bcb213fe1 2ae1f873eb
Коммит 492e6530dc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 1266 добавлений и 115 удалений

Просмотреть файл

@ -3,6 +3,7 @@ build
.github
lib/*.js.map
lib/*.js
lib/test
!lib/*.min.js
node_modules
src
@ -14,4 +15,5 @@ tsfmt.json
.editorconfig
.esbuild.config.js
.eslintrc.json
.eslintignore
.eslintignore
*.test.ts

1094
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -14,17 +14,20 @@
},
"scripts": {
"build": "node .esbuild.config.js",
"test": "tsc -p 'test/tsconfig.json' && mocha lib/test/*",
"compile": "tsc -noEmit -p 'src/browser/tsconfig.json' && tsc -noEmit -p 'src/node/tsconfig.json' && npm run build"
},
"devDependencies": {
"@microsoft/applicationinsights-web": "^2.6.4",
"@types/node": "^16.9.6",
"@types/mocha": "^9.1.1",
"@types/node": "^16.11.32",
"@types/vscode": "^1.60.0",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"applicationinsights": "2.1.4",
"esbuild": "^0.14.32",
"eslint": "^8.12.0",
"mocha": "^9.2.2",
"typescript": "^4.6.2"
},
"repository": {

Просмотреть файл

@ -3,10 +3,10 @@
*--------------------------------------------------------*/
import type { ApplicationInsights } from "@microsoft/applicationinsights-web";
import * as vscode from "vscode";
import { BaseTelemetryAppender, BaseTelemetryClient } from "../common/baseTelemetryAppender";
import { AppenderData, BaseTelemetryReporter, ReplacementOption } from "../common/baseTelemetryReporter";
import { applyReplacements } from "../common/util";
import { TelemetryUtil } from "../common/util";
const webAppInsightsClientFactory = async (key: string, replacementOptions?: ReplacementOption[]): Promise<BaseTelemetryClient> => {
let appInsightsClient: ApplicationInsights | undefined;
@ -44,7 +44,7 @@ const webAppInsightsClientFactory = async (key: string, replacementOptions?: Rep
logEvent: (eventName: string, data?: AppenderData) => {
const properties = { ...data?.properties, ...data?.measurements };
if (replacementOptions?.length) {
applyReplacements(properties, replacementOptions);
TelemetryUtil.applyReplacements(properties, replacementOptions);
}
appInsightsClient?.trackEvent(
{ name: eventName },
@ -54,7 +54,7 @@ const webAppInsightsClientFactory = async (key: string, replacementOptions?: Rep
logException: (exception: Error, data?: AppenderData) => {
const properties = { ...data?.properties, ...data?.measurements };
if (replacementOptions?.length) {
applyReplacements(properties, replacementOptions);
TelemetryUtil.applyReplacements(properties, replacementOptions);
}
appInsightsClient?.trackException(
{
@ -79,6 +79,6 @@ export default class TelemetryReporter extends BaseTelemetryReporter {
release: navigator.appVersion,
platform: "web",
architecture: "web",
}, firstParty);
}, vscode, firstParty);
}
}

Просмотреть файл

@ -3,7 +3,6 @@
*--------------------------------------------------------*/
import { AppenderData } from "./baseTelemetryReporter";
import { getTelemetryLevel, TelemetryLevel } from "./util";
export interface BaseTelemetryClient {
logEvent(eventName: string, data?: AppenderData): void;
@ -37,12 +36,12 @@ export class BaseTelemetryAppender implements ITelemetryAppender {
private _clientFactory: (key: string) => Promise<BaseTelemetryClient>;
private _key: string;
constructor(key: string, clientFactory: (key: string) => Promise<BaseTelemetryClient>) {
constructor(
key: string,
clientFactory: (key: string) => Promise<BaseTelemetryClient>,
) {
this._clientFactory = clientFactory;
this._key = key;
if (getTelemetryLevel() !== TelemetryLevel.OFF) {
this.instantiateAppender();
}
}
/**

Просмотреть файл

@ -2,10 +2,10 @@
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as vscode from "vscode";
import type * as vscode from "vscode";
import type { TelemetryEventMeasurements, TelemetryEventProperties, RawTelemetryEventProperties } from "../../lib/telemetryReporter";
import { ITelemetryAppender } from "./baseTelemetryAppender";
import { getTelemetryLevel, TelemetryLevel } from "./util";
import { TelemetryLevel, TelemetryUtil } from "./util";
export interface AppenderData {
properties?: RawTelemetryEventProperties,
@ -40,17 +40,18 @@ export class BaseTelemetryReporter {
private extensionVersion: string,
private telemetryAppender: ITelemetryAppender,
private osShim: { release: string, platform: string, architecture: string },
private readonly vscodeAPI: typeof vscode,
firstParty?: boolean
) {
this.firstParty = !!firstParty;
this.updateUserOptStatus();
if (vscode.env.onDidChangeTelemetryEnabled !== undefined) {
this.disposables.push(vscode.env.onDidChangeTelemetryEnabled(() => this.updateUserOptStatus()));
this.disposables.push(vscode.workspace.onDidChangeConfiguration(() => this.updateUserOptStatus()));
if (vscodeAPI.env.onDidChangeTelemetryEnabled !== undefined) {
this.disposables.push(vscodeAPI.env.onDidChangeTelemetryEnabled(() => this.updateUserOptStatus()));
this.disposables.push(vscodeAPI.workspace.onDidChangeConfiguration(() => this.updateUserOptStatus()));
} else {
this.disposables.push(vscode.workspace.onDidChangeConfiguration(() => this.updateUserOptStatus()));
this.disposables.push(vscodeAPI.workspace.onDidChangeConfiguration(() => this.updateUserOptStatus()));
}
}
@ -58,7 +59,7 @@ export class BaseTelemetryReporter {
* Updates whether the user has opted in to having telemetry collected
*/
private updateUserOptStatus(): void {
const telemetryLevel = getTelemetryLevel();
const telemetryLevel = TelemetryUtil.getInstance(this.vscodeAPI).getTelemetryLevel();
this.userOptIn = telemetryLevel === TelemetryLevel.ON;
this.errorOptIn = telemetryLevel === TelemetryLevel.ERROR || this.userOptIn;
if (this.userOptIn || this.errorOptIn) {
@ -92,7 +93,7 @@ export class BaseTelemetryReporter {
*/
private get extension(): vscode.Extension<any> | undefined {
if (this._extension === undefined) {
this._extension = vscode.extensions.getExtension(this.extensionId);
this._extension = this.vscodeAPI.extensions.getExtension(this.extensionId);
}
return this._extension;
@ -126,7 +127,7 @@ export class BaseTelemetryReporter {
if (this.firstParty) {
// Don't collect errors from unknown remotes
if (vscode.env.remoteName && this.cleanRemoteName(vscode.env.remoteName) === "other") {
if (this.vscodeAPI.env.remoteName && this.cleanRemoteName(this.vscodeAPI.env.remoteName) === "other") {
return false;
}
@ -154,25 +155,25 @@ export class BaseTelemetryReporter {
commonProperties["common.platformversion"] = (this.osShim.release || "").replace(/^(\d+)(\.\d+)?(\.\d+)?(.*)/, "$1$2$3");
commonProperties["common.extname"] = this.extensionId;
commonProperties["common.extversion"] = this.extensionVersion;
if (vscode && vscode.env) {
commonProperties["common.vscodemachineid"] = vscode.env.machineId;
commonProperties["common.vscodesessionid"] = vscode.env.sessionId;
commonProperties["common.vscodeversion"] = vscode.version;
commonProperties["common.isnewappinstall"] = vscode.env.isNewAppInstall ? vscode.env.isNewAppInstall.toString() : "false";
commonProperties["common.product"] = vscode.env.appHost;
if (this.vscodeAPI && this.vscodeAPI.env) {
commonProperties["common.vscodemachineid"] = this.vscodeAPI.env.machineId;
commonProperties["common.vscodesessionid"] = this.vscodeAPI.env.sessionId;
commonProperties["common.vscodeversion"] = this.vscodeAPI.version;
commonProperties["common.isnewappinstall"] = this.vscodeAPI.env.isNewAppInstall ? this.vscodeAPI.env.isNewAppInstall.toString() : "false";
commonProperties["common.product"] = this.vscodeAPI.env.appHost;
switch (vscode.env.uiKind) {
case vscode.UIKind.Web:
switch (this.vscodeAPI.env.uiKind) {
case this.vscodeAPI.UIKind.Web:
commonProperties["common.uikind"] = "web";
break;
case vscode.UIKind.Desktop:
case this.vscodeAPI.UIKind.Desktop:
commonProperties["common.uikind"] = "desktop";
break;
default:
commonProperties["common.uikind"] = "unknown";
}
commonProperties["common.remotename"] = this.cleanRemoteName(vscode.env.remoteName);
commonProperties["common.remotename"] = this.cleanRemoteName(this.vscodeAPI.env.remoteName);
}
return commonProperties;
}
@ -190,8 +191,8 @@ export class BaseTelemetryReporter {
}
const cleanupPatterns = [];
if (vscode.env.appRoot !== "") {
cleanupPatterns.push(new RegExp(vscode.env.appRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"));
if (this.vscodeAPI.env.appRoot !== "") {
cleanupPatterns.push(new RegExp(this.vscodeAPI.env.appRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"));
}
if (this.extension) {
cleanupPatterns.push(new RegExp(this.extension.extensionPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"));
@ -271,7 +272,7 @@ export class BaseTelemetryReporter {
}
public get telemetryLevel(): "all" | "error" | "crash" | "off" {
const telemetryLevel = getTelemetryLevel();
const telemetryLevel = TelemetryUtil.getInstance(this.vscodeAPI).getTelemetryLevel();
switch (telemetryLevel) {
case TelemetryLevel.ON:
return "all";

Просмотреть файл

@ -2,8 +2,8 @@
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as vscode from "vscode";
import { ReplacementOption } from "./baseTelemetryReporter";
import type * as vscode from "vscode";
import type { ReplacementOption } from "./baseTelemetryReporter";
export const enum TelemetryLevel {
ON = "on",
@ -11,42 +11,56 @@ export const enum TelemetryLevel {
OFF = "off"
}
export function getTelemetryLevel(): TelemetryLevel {
const TELEMETRY_CONFIG_ID = "telemetry";
const TELEMETRY_CONFIG_ENABLED_ID = "enableTelemetry";
export class TelemetryUtil {
private static _instance: TelemetryUtil | undefined;
try {
const telemetryConfiguration = vscode.env.telemetryConfiguration;
if (telemetryConfiguration.isUsageEnabled && telemetryConfiguration.isErrorsEnabled && telemetryConfiguration.isCrashEnabled) {
return TelemetryLevel.ON;
} else if (telemetryConfiguration.isErrorsEnabled && telemetryConfiguration.isCrashEnabled) {
return TelemetryLevel.ERROR;
} else {
return TelemetryLevel.OFF;
}
} catch {
// Could be undefined in old versions of vs code
if (vscode.env.isTelemetryEnabled !== undefined) {
return vscode.env.isTelemetryEnabled ? TelemetryLevel.ON : TelemetryLevel.OFF;
}
constructor (private readonly vscodeAPI: typeof vscode) { }
// We use the old and new setting to determine the telemetry level as we must respect both
const config = vscode.workspace.getConfiguration(TELEMETRY_CONFIG_ID);
const enabled = config.get<boolean>(TELEMETRY_CONFIG_ENABLED_ID);
return enabled ? TelemetryLevel.ON : TelemetryLevel.OFF;
public getTelemetryLevel(): TelemetryLevel {
const TELEMETRY_CONFIG_ID = "telemetry";
const TELEMETRY_CONFIG_ENABLED_ID = "enableTelemetry";
try {
const telemetryConfiguration = this.vscodeAPI.env.telemetryConfiguration;
if (telemetryConfiguration.isUsageEnabled && telemetryConfiguration.isErrorsEnabled && telemetryConfiguration.isCrashEnabled) {
return TelemetryLevel.ON;
} else if (telemetryConfiguration.isErrorsEnabled && telemetryConfiguration.isCrashEnabled) {
return TelemetryLevel.ERROR;
} else {
return TelemetryLevel.OFF;
}
} catch {
// Could be undefined in old versions of vs code
if (this.vscodeAPI.env.isTelemetryEnabled !== undefined) {
return this.vscodeAPI.env.isTelemetryEnabled ? TelemetryLevel.ON : TelemetryLevel.OFF;
}
// We use the old and new setting to determine the telemetry level as we must respect both
const config = this.vscodeAPI.workspace.getConfiguration(TELEMETRY_CONFIG_ID);
const enabled = config.get<boolean>(TELEMETRY_CONFIG_ENABLED_ID);
return enabled ? TelemetryLevel.ON : TelemetryLevel.OFF;
}
}
}
export function applyReplacements(data: Record<string, any>, replacementOptions: ReplacementOption[]) {
for (const key of Object.keys(data)) {
for (const option of replacementOptions) {
if (option.lookup.test(key)) {
if (option.replacementString !== undefined) {
data[key] = option.replacementString;
} else {
delete data[key];
public static applyReplacements(data: Record<string, any>, replacementOptions: ReplacementOption[]) {
for (const key of Object.keys(data)) {
for (const option of replacementOptions) {
if (option.lookup.test(key)) {
if (option.replacementString !== undefined) {
data[key] = option.replacementString;
} else {
delete data[key];
}
}
}
}
}
// Get singleton instance of TelemetryUtil
public static getInstance(vscodeAPI: typeof vscode): TelemetryUtil {
if (!TelemetryUtil._instance) {
TelemetryUtil._instance = new TelemetryUtil(vscodeAPI);
}
return TelemetryUtil._instance;
}
}

Просмотреть файл

@ -7,8 +7,7 @@ import * as vscode from "vscode";
import type { TelemetryClient } from "applicationinsights";
import { AppenderData, BaseTelemetryReporter, ReplacementOption } from "../common/baseTelemetryReporter";
import { BaseTelemetryAppender, BaseTelemetryClient } from "../common/baseTelemetryAppender";
import { applyReplacements } from "../common/util";
import { TelemetryUtil } from "../common/util";
/**
* A factory function which creates a telemetry client to be used by an appender to send telemetry in a node application.
*
@ -107,13 +106,13 @@ const appInsightsClientFactory = async (key: string, replacementOptions?: Replac
function addReplacementOptions(appInsightsClient: TelemetryClient, replacementOptions: ReplacementOption[]) {
appInsightsClient.addTelemetryProcessor((event) => {
if (Array.isArray(event.tags)) {
event.tags.forEach(tag => applyReplacements(tag, replacementOptions));
event.tags.forEach(tag => TelemetryUtil.applyReplacements(tag, replacementOptions));
} else if (event.tags) {
applyReplacements(event.tags, replacementOptions);
TelemetryUtil.applyReplacements(event.tags, replacementOptions);
}
if (event.data.baseData) {
applyReplacements(event.data.baseData, replacementOptions);
TelemetryUtil.applyReplacements(event.data.baseData, replacementOptions);
}
return true;
});
@ -129,6 +128,6 @@ export default class TelemetryReporter extends BaseTelemetryReporter {
release: os.release(),
platform: os.platform(),
architecture: os.arch(),
}, firstParty);
}, vscode, firstParty);
}
}

13
test/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
// Must include DOM to make the node type "URL" known about
"lib": ["ES2015", "DOM"],
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "../lib/test",
// Not sure why it cannot find them, but alas it must be pointed out where the types are
"typeRoots": ["../node_modules/@types"]
},
"include": [".", "../vscode.proposed.telemetry.d.ts"]
}

112
test/util.test.ts Normal file
Просмотреть файл

@ -0,0 +1,112 @@
import * as assert from "assert";
import type * as vscode from "vscode";
import { TelemetryUtil, TelemetryLevel } from "../src/common/util";
describe("Util test suite", () => {
// Test that the apply repacements util function works as expected
it("Apply replacements", () => {
let replacement: Record<string, any> = {};
TelemetryUtil.applyReplacements(replacement, []);
assert.deepStrictEqual(replacement, {});
replacement = { valueA: "a", valueB: "b", "123": "123" };
TelemetryUtil.applyReplacements(replacement, [{
lookup: /[a]/gi,
replacementString: "c"
}]);
assert.deepStrictEqual(replacement, { valueA: "c", valueB: "b", "123": "123" });
replacement = { valueA: "a", valueB: "b", "123": "123" };
TelemetryUtil.applyReplacements(replacement, [{
lookup: /[a]/gi,
replacementString: undefined
}]);
// Undefined replacement string should remove the key
assert.deepStrictEqual(replacement, { valueB: "b", "123": "123" });
});
it("Telemetry util implements singleton", () => {
const vscodeAPI = {} as typeof vscode;
const telemetryUtil1 = TelemetryUtil.getInstance(vscodeAPI);
const telemetryUtil2 = TelemetryUtil.getInstance(vscodeAPI);
assert.strictEqual(telemetryUtil1, telemetryUtil2);
});
it("Get telemetry level - Using telemetry configuration API", () => {
// Casting here is required to not have to shim the whole API
// Decently safe as tests will just fail if not enough API is available
let telemetryShim = {
env: {
telemetryConfiguration: {
isUsageEnabled: true,
isErrorsEnabled: true,
isCrashEnabled: true
},
isTelemetryEnabled: false
}
} as typeof vscode;
// Make a new telemetry util as we don't want to use getInstance as it will not
// update with new API shims passed in subsequent tests
let telemetryUtil = new TelemetryUtil(telemetryShim);
assert.strictEqual(telemetryUtil.getTelemetryLevel(), TelemetryLevel.ON);
telemetryShim = {
env: {
telemetryConfiguration: {
isUsageEnabled: false,
isErrorsEnabled: true,
isCrashEnabled: true
}
}
} as typeof vscode;
telemetryUtil = new TelemetryUtil(telemetryShim);
assert.strictEqual(telemetryUtil.getTelemetryLevel(), TelemetryLevel.ERROR);
telemetryShim = {
env: {
telemetryConfiguration: {
isUsageEnabled: false,
isErrorsEnabled: false,
isCrashEnabled: true
}
}
} as typeof vscode;
telemetryUtil = new TelemetryUtil(telemetryShim);
// Extensions don't support crash so it should report off
assert.strictEqual(telemetryUtil.getTelemetryLevel(), TelemetryLevel.OFF);
telemetryShim = {
env: {
telemetryConfiguration: {
isUsageEnabled: false,
isErrorsEnabled: false,
isCrashEnabled: false
}
}
} as typeof vscode;
telemetryUtil = new TelemetryUtil(telemetryShim);
assert.strictEqual(telemetryUtil.getTelemetryLevel(), TelemetryLevel.OFF);
});
it ("Get telemetry level - Using telemetry enabled API", () => {
// Casting here is required to not have to shim the whole API
// Decently safe as tests will just fail if not enough API is available
let telemetryShim = {
env: {
isTelemetryEnabled: true
}
} as typeof vscode;
// Make a new telemetry util as we don't want to use getInstance as it will not
// update with new API shims passed in subsequent tests
let telemetryUtil = new TelemetryUtil(telemetryShim);
assert.strictEqual(telemetryUtil.getTelemetryLevel(), TelemetryLevel.ON);
telemetryShim = {
env: {
isTelemetryEnabled: false
}
} as typeof vscode;
telemetryUtil = new TelemetryUtil(telemetryShim);
assert.strictEqual(telemetryUtil.getTelemetryLevel(), TelemetryLevel.OFF);
});
// TODO - Add tests for when you just have telemetry configuration settings and no API
// This is the hardest to shim and only in very old versions of VS Code
});