Initial scaffolding for a web version of the module

This commit is contained in:
Logan Ramos 2021-07-16 16:00:34 -04:00
Родитель 260c7c3a5a
Коммит bad27f4eca
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: D9CCFF14F0B18183
21 изменённых файлов: 29230 добавлений и 698 удалений

12
.editorconfig Normal file
Просмотреть файл

@ -0,0 +1,12 @@
# top-most EditorConfig file
root = true
# Tab indentation
[*]
indent_style = tab
trim_trailing_whitespace = true
# The indent size used in the `package.json` file cannot be changed
[{*.yml,*.yaml,package.json}]
indent_style = space
indent_size = 2

41
.esbuild.config.js Normal file
Просмотреть файл

@ -0,0 +1,41 @@
const esbuild = require('esbuild');
// Build node packages and their minifed versions
esbuild.build({
entryPoints: ['src/node/telemetryReporter.ts'],
bundle: true,
external: ['vscode'],
sourcemap: true,
platform: 'node',
outfile: 'lib/telemetryReporter.node.js',
}).catch(() => process.exit(1))
esbuild.build({
entryPoints: ['src/node/telemetryReporter.ts'],
bundle: true,
sourcemap: false,
external: ['vscode'],
minify: true,
platform: 'node',
outfile: 'lib/telemetryReporter.node.min.js',
}).catch(() => process.exit(1))
// Build browser packages and their minified versions
esbuild.build({
entryPoints: ['src/browser/telemetryReporter.ts'],
bundle: true,
sourcemap: true,
external: ['vscode'],
platform: 'browser',
outfile: 'lib/telemetryReporter.web.js',
}).catch(() => process.exit(1))
esbuild.build({
entryPoints: ['src/browser/telemetryReporter.ts'],
bundle: true,
sourcemap: false,
external: ['vscode'],
minify: true,
platform: 'browser',
outfile: 'lib/telemetryReporter.web.min.js',
}).catch(() => process.exit(1))

3
.eslintignore Normal file
Просмотреть файл

@ -0,0 +1,3 @@
node_modules
*.d.ts
*.js

39
.eslintrc.json Normal file
Просмотреть файл

@ -0,0 +1,39 @@
{
"env": {
"node": true,
"commonjs": true,
"es6": true,
"jest": true,
"browser": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018
},
"plugins": [
"@typescript-eslint/eslint-plugin"
],
"rules": {
"quotes": ["error", "double"],
"semi": ["error", "always"],
"no-console": "off",
"no-var": 1,
"no-case-declarations": 0,
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/camelcase": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"object-curly-spacing": [
2,
"always"
]
}
}

1
.vscode/settings.json поставляемый
Просмотреть файл

@ -6,5 +6,4 @@
"**/.DS_Store": true,
"**/*.js": {"when": "$(basename).ts"}
},
"eslint.enable": false
}

25
.vscode/tasks.json поставляемый
Просмотреть файл

@ -1,9 +1,24 @@
{
"version": "0.1.0",
"version": "2.0.0",
"command": "npm",
"isShellCommand": true,
"args": ["run", "watch"],
"showOutput": "silent",
"args": [
"run",
"watch"
],
"isBackground": true,
"problemMatcher": "$tsc-watch"
"problemMatcher": "$tsc-watch",
"tasks": [
{
"label": "npm",
"type": "shell",
"command": "npm",
"args": [
"run",
"watch"
],
"isBackground": true,
"problemMatcher": "$tsc-watch",
"group": "build"
}
]
}

18
lib/telemetryReporter.d.ts поставляемый
Просмотреть файл

@ -1,23 +1,5 @@
export default class TelemetryReporter {
private extensionId;
private extensionVersion;
private appInsightsClient;
private firstParty;
private userOptIn;
private _extension;
private readonly optOutListener;
private static TELEMETRY_CONFIG_ID;
private static TELEMETRY_CONFIG_ENABLED_ID;
private logStream;
constructor(extensionId: string, extensionVersion: string, key: string, firstParty?: boolean);
private updateUserOptIn;
private createAppInsightsClient;
private getCommonProperties;
private cleanRemoteName;
private shouldSendErrorTelemetry;
private get extension();
private cloneAndChange;
private anonymizeFilePaths;
sendTelemetryEvent(eventName: string, properties?: {
[key: string]: string;
}, measurements?: {

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

@ -1,303 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
process.env['APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL'] = true;
var fs = require("fs");
var os = require("os");
var path = require("path");
var vscode = require("vscode");
var appInsights = require("applicationinsights");
var TelemetryReporter = /** @class */ (function () {
// tslint:disable-next-line
function TelemetryReporter(extensionId, extensionVersion, key, firstParty) {
var _this = this;
this.extensionId = extensionId;
this.extensionVersion = extensionVersion;
this.firstParty = false;
this.userOptIn = false;
this.firstParty = !!firstParty;
var logFilePath = process.env['VSCODE_LOGS'] || '';
if (logFilePath && extensionId && process.env['VSCODE_LOG_LEVEL'] === 'trace') {
logFilePath = path.join(logFilePath, extensionId + ".txt");
this.logStream = fs.createWriteStream(logFilePath, { flags: 'a', encoding: 'utf8', autoClose: true });
}
this.updateUserOptIn(key);
if (vscode.env.onDidChangeTelemetryEnabled !== undefined) {
this.optOutListener = vscode.env.onDidChangeTelemetryEnabled(function () { return _this.updateUserOptIn(key); });
}
else {
this.optOutListener = vscode.workspace.onDidChangeConfiguration(function () { return _this.updateUserOptIn(key); });
}
}
TelemetryReporter.prototype.updateUserOptIn = function (key) {
// Newer versions of vscode api have telemetry enablement exposed, but fallback to setting for older versions
var config = vscode.workspace.getConfiguration(TelemetryReporter.TELEMETRY_CONFIG_ID);
var newOptInValue = vscode.env.isTelemetryEnabled === undefined ?
config.get(TelemetryReporter.TELEMETRY_CONFIG_ENABLED_ID, true) :
vscode.env.isTelemetryEnabled;
if (this.userOptIn !== newOptInValue) {
this.userOptIn = newOptInValue;
if (this.userOptIn) {
this.createAppInsightsClient(key);
}
else {
this.dispose();
}
}
};
TelemetryReporter.prototype.createAppInsightsClient = function (key) {
//check if another instance is already initialized
if (appInsights.defaultClient) {
this.appInsightsClient = new appInsights.TelemetryClient(key);
// no other way to enable offline mode
this.appInsightsClient.channel.setUseDiskRetryCaching(true);
}
else {
appInsights.setup(key)
.setAutoCollectRequests(false)
.setAutoCollectPerformance(false)
.setAutoCollectExceptions(false)
.setAutoCollectDependencies(false)
.setAutoDependencyCorrelation(false)
.setAutoCollectConsole(false)
.setUseDiskRetryCaching(true)
.start();
this.appInsightsClient = appInsights.defaultClient;
}
this.appInsightsClient.commonProperties = this.getCommonProperties();
if (vscode && vscode.env) {
this.appInsightsClient.context.tags[this.appInsightsClient.context.keys.userId] = vscode.env.machineId;
this.appInsightsClient.context.tags[this.appInsightsClient.context.keys.sessionId] = vscode.env.sessionId;
}
//check if it's an Asimov key to change the endpoint
if (key && key.indexOf('AIF-') === 0) {
this.appInsightsClient.config.endpointUrl = "https://vortex.data.microsoft.com/collect/v1";
this.firstParty = true;
}
};
// __GDPR__COMMON__ "common.os" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.platformversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.extname" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.extversion" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.vscodemachineid" : { "endPoint": "MacAddressHash", "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.vscodesessionid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.vscodeversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.isnewappinstall" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
TelemetryReporter.prototype.getCommonProperties = function () {
var commonProperties = Object.create(null);
commonProperties['common.os'] = os.platform();
commonProperties['common.platformversion'] = (os.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;
switch (vscode.env.uiKind) {
case vscode.UIKind.Web:
commonProperties['common.uikind'] = 'web';
break;
case vscode.UIKind.Desktop:
commonProperties['common.uikind'] = 'desktop';
break;
default:
commonProperties['common.uikind'] = 'unknown';
}
commonProperties['common.remotename'] = this.cleanRemoteName(vscode.env.remoteName);
}
return commonProperties;
};
TelemetryReporter.prototype.cleanRemoteName = function (remoteName) {
if (!remoteName) {
return 'none';
}
var ret = 'other';
// Allowed remote authorities
['ssh-remote', 'dev-container', 'attached-container', 'wsl'].forEach(function (res) {
if (remoteName.indexOf(res + "+") === 0) {
ret = res;
}
});
return ret;
};
TelemetryReporter.prototype.shouldSendErrorTelemetry = function () {
if (this.firstParty) {
if (this.cleanRemoteName(vscode.env.remoteName) !== 'other') {
return true;
}
if (this.extension === undefined || this.extension.extensionKind === vscode.ExtensionKind.Workspace) {
return false;
}
if (vscode.env.uiKind === vscode.UIKind.Web) {
return false;
}
return true;
}
return true;
};
Object.defineProperty(TelemetryReporter.prototype, "extension", {
get: function () {
if (this._extension === undefined) {
this._extension = vscode.extensions.getExtension(this.extensionId);
}
return this._extension;
},
enumerable: false,
configurable: true
});
TelemetryReporter.prototype.cloneAndChange = function (obj, change) {
if (obj === null || typeof obj !== 'object')
return obj;
if (typeof change !== 'function')
return obj;
var ret = {};
for (var key in obj) {
ret[key] = change(key, obj[key]);
}
return ret;
};
TelemetryReporter.prototype.anonymizeFilePaths = function (stack, anonymizeFilePaths) {
if (stack === undefined || stack === null) {
return '';
}
var cleanupPatterns = [new RegExp(vscode.env.appRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')];
if (this.extension) {
cleanupPatterns.push(new RegExp(this.extension.extensionPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'));
}
var updatedStack = stack;
if (anonymizeFilePaths) {
var cleanUpIndexes = [];
for (var _i = 0, cleanupPatterns_1 = cleanupPatterns; _i < cleanupPatterns_1.length; _i++) {
var regexp = cleanupPatterns_1[_i];
while (true) {
var result = regexp.exec(stack);
if (!result) {
break;
}
cleanUpIndexes.push([result.index, regexp.lastIndex]);
}
}
var nodeModulesRegex = /^[\\\/]?(node_modules|node_modules\.asar)[\\\/]/;
var fileRegex = /(file:\/\/)?([a-zA-Z]:(\\\\|\\|\/)|(\\\\|\\|\/))?([\w-\._]+(\\\\|\\|\/))+[\w-\._]*/g;
var lastIndex = 0;
updatedStack = '';
var _loop_1 = function () {
var result = fileRegex.exec(stack);
if (!result) {
return "break";
}
// Anoynimize user file paths that do not need to be retained or cleaned up.
if (!nodeModulesRegex.test(result[0]) && cleanUpIndexes.every(function (_a) {
var x = _a[0], y = _a[1];
return result.index < x || result.index >= y;
})) {
updatedStack += stack.substring(lastIndex, result.index) + '<REDACTED: user-file-path>';
lastIndex = fileRegex.lastIndex;
}
};
while (true) {
var state_1 = _loop_1();
if (state_1 === "break")
break;
}
if (lastIndex < stack.length) {
updatedStack += stack.substr(lastIndex);
}
}
// sanitize with configured cleanup patterns
for (var _a = 0, cleanupPatterns_2 = cleanupPatterns; _a < cleanupPatterns_2.length; _a++) {
var regexp = cleanupPatterns_2[_a];
updatedStack = updatedStack.replace(regexp, '');
}
return updatedStack;
};
TelemetryReporter.prototype.sendTelemetryEvent = function (eventName, properties, measurements) {
var _this = this;
if (this.userOptIn && eventName && this.appInsightsClient) {
var cleanProperties = this.cloneAndChange(properties, function (_key, prop) { return _this.anonymizeFilePaths(prop, _this.firstParty); });
this.appInsightsClient.trackEvent({
name: this.extensionId + "/" + eventName,
properties: cleanProperties,
measurements: measurements
});
if (this.logStream) {
this.logStream.write("telemetry/" + eventName + " " + JSON.stringify({ properties: properties, measurements: measurements }) + "\n");
}
}
};
TelemetryReporter.prototype.sendTelemetryErrorEvent = function (eventName, properties, measurements, errorProps) {
var _this = this;
if (this.userOptIn && eventName && this.appInsightsClient) {
// always clean the properties if first party
// do not send any error properties if we shouldn't send error telemetry
// if we have no errorProps, assume all are error props
var cleanProperties = this.cloneAndChange(properties, function (key, prop) {
if (_this.shouldSendErrorTelemetry()) {
return _this.anonymizeFilePaths(prop, _this.firstParty);
}
if (errorProps === undefined || errorProps.indexOf(key) !== -1) {
return 'REDACTED';
}
return _this.anonymizeFilePaths(prop, _this.firstParty);
});
this.appInsightsClient.trackEvent({
name: this.extensionId + "/" + eventName,
properties: cleanProperties,
measurements: measurements
});
if (this.logStream) {
this.logStream.write("telemetry/" + eventName + " " + JSON.stringify({ properties: properties, measurements: measurements }) + "\n");
}
}
};
TelemetryReporter.prototype.sendTelemetryException = function (error, properties, measurements) {
var _this = this;
if (this.shouldSendErrorTelemetry() && this.userOptIn && error && this.appInsightsClient) {
var cleanProperties = this.cloneAndChange(properties, function (_key, prop) { return _this.anonymizeFilePaths(prop, _this.firstParty); });
this.appInsightsClient.trackException({
exception: error,
properties: cleanProperties,
measurements: measurements
});
if (this.logStream) {
this.logStream.write("telemetry/" + error.name + " " + error.message + " " + JSON.stringify({ properties: properties, measurements: measurements }) + "\n");
}
}
};
TelemetryReporter.prototype.dispose = function () {
var _this = this;
this.optOutListener.dispose();
var flushEventsToLogger = new Promise(function (resolve) {
if (!_this.logStream) {
return resolve(void 0);
}
_this.logStream.on('finish', resolve);
_this.logStream.end();
});
var flushEventsToAI = new Promise(function (resolve) {
if (_this.appInsightsClient) {
_this.appInsightsClient.flush({
callback: function () {
// all data flushed
_this.appInsightsClient = undefined;
resolve(void 0);
}
});
}
else {
resolve(void 0);
}
});
return Promise.all([flushEventsToAI, flushEventsToLogger]);
};
TelemetryReporter.TELEMETRY_CONFIG_ID = 'telemetry';
TelemetryReporter.TELEMETRY_CONFIG_ENABLED_ID = 'enableTelemetry';
return TelemetryReporter;
}());
exports.default = TelemetryReporter;
//# sourceMappingURL=telemetryReporter.js.map

16787
lib/telemetryReporter.node.js Normal file

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

10
lib/telemetryReporter.node.min.js поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

7894
lib/telemetryReporter.web.js Normal file

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

12
lib/telemetryReporter.web.min.js поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

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

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

@ -1,26 +1,33 @@
{
"name": "vscode-extension-telemetry",
"description": "A module for first party microsoft extensions to report consistent telemetry.",
"version": "0.1.7",
"version": "0.2.0",
"author": {
"name": "Microsoft Corporation"
},
"main": "./lib/telemetryReporter.js",
"main": "./lib/telemetryReporter.node.min.js",
"browser": "./lib/telemetryReporter.web.min.js",
"types": "./lib/telemetryReporter.d.ts",
"license": "MIT",
"engines": {
"vscode": "^1.5.0"
},
"scripts": {
"build": "node .esbuild.config.js",
"compile": "tsc -p .",
"watch": "tsc -w -p ."
},
"dependencies": {
"applicationinsights": "1.7.4"
"@microsoft/applicationinsights-web": "^2.6.4",
"applicationinsights": "2.1.4"
},
"devDependencies": {
"@types/node": "^12.15.0",
"@types/vscode": "^1.40.0",
"@types/vscode": "^1.50.0",
"@typescript-eslint/eslint-plugin": "^4.28.3",
"@typescript-eslint/parser": "^4.28.3",
"esbuild": "^0.12.15",
"eslint": "^7.30.0",
"typescript": "^4.2.3"
},
"repository": {

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

@ -0,0 +1,71 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import { ApplicationInsights } from '@microsoft/applicationinsights-web';
import { BaseTelemtryReporter, ITelemetryAppender } from '../common/baseTelemetryReporter';
class WebAppInsightsAppender implements ITelemetryAppender {
private _aiClient: ApplicationInsights | undefined;
constructor(key: string) {
let endpointUrl: undefined | string;
if (key && key.indexOf('AIF-') === 0) {
endpointUrl = 'https://vortex.data.microsoft.com/collect/v1';
}
this._aiClient = new ApplicationInsights({
config: {
instrumentationKey: key,
endpointUrl,
disableAjaxTracking: true,
disableExceptionTracking: true,
disableFetchTracking: true,
disableCorrelationHeaders: true,
disableCookiesUsage: true,
autoTrackPageVisitTime: false,
emitLineDelimitedJson: true,
},
});
this._aiClient.loadAppInsights();
// If we cannot access the endpoint this most likely means it's being blocked
// and we should not attempt to send any telemetry.
if (endpointUrl) {
fetch(endpointUrl).catch(() => (this._aiClient = undefined));
}
}
public logEvent(eventName: string, data: any): void {
if (!this._aiClient) {
return;
}
this._aiClient.trackEvent({name: eventName}, {...data.properties, ...data.measurements});
}
public logException(exception: Error, data: any): void {
if (!this._aiClient) {
return;
}
this._aiClient.trackException({exception, properties: {...data.properties, ...data.measurements}});
}
public flush(): Promise<any> {
if (this._aiClient) {
this._aiClient.flush();
this._aiClient = undefined;
}
return Promise.resolve(undefined);
}
}
export default class TelemetryReporter extends BaseTelemtryReporter {
constructor(extensionId: string, extensionVersion: string, key: string, firstParty?: boolean) {
const appender = new WebAppInsightsAppender(key);
if (key && key.indexOf('AIF-') === 0) {
firstParty = true;
}
super(extensionId, extensionVersion, appender, { release: navigator.appVersion, platform: 'web' }, firstParty);
}
}

12
src/browser/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["ES2015","DOM"],
"target": "ES6",
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "./lib",
"typeRoots": []
},
"include": [".", "../common"]
}

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

@ -0,0 +1,286 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as vscode from 'vscode';
export interface ITelemetryAppender {
logEvent(eventName: string, data?: any): void;
logException(exception: Error, data?: any): void;
flush(): void | Promise<void>;
}
export class BaseTelemtryReporter {
private firstParty: boolean = false;
private userOptIn: boolean = false;
private _extension: vscode.Extension<any> | undefined;
private readonly optOutListener: vscode.Disposable;
private static TELEMETRY_CONFIG_ID = 'telemetry';
private static TELEMETRY_CONFIG_ENABLED_ID = 'enableTelemetry';
constructor(
private extensionId: string,
private extensionVersion: string,
private telemetryAppender: ITelemetryAppender,
private osShim: { release: string, platform: string },
firstParty?: boolean
) {
this.firstParty = !!firstParty;
this.updateUserOptStatus();
if (vscode.env.onDidChangeTelemetryEnabled !== undefined) {
this.optOutListener = vscode.env.onDidChangeTelemetryEnabled(() => this.updateUserOptStatus());
} else {
this.optOutListener = vscode.workspace.onDidChangeConfiguration(() => this.updateUserOptStatus());
}
}
/**
* Updates whether the user has opted in to having telemetry collected
*/
private updateUserOptStatus(): void {
// Newer versions of vscode api have telemetry enablement exposed, but fallback to setting for older versions
const config = vscode.workspace.getConfiguration(BaseTelemtryReporter.TELEMETRY_CONFIG_ID);
const newOptInValue = vscode.env.isTelemetryEnabled === undefined ?
config.get<boolean>(BaseTelemtryReporter.TELEMETRY_CONFIG_ENABLED_ID, true) :
vscode.env.isTelemetryEnabled;
if (this.userOptIn !== newOptInValue) {
this.userOptIn = newOptInValue;
}
}
/**
* Given a remoteName ensures it is in the list of valid ones
* @param remoteName The remotename
* @returns The "cleaned" one
*/
private cleanRemoteName(remoteName?: string): string {
if (!remoteName) {
return 'none';
}
let ret = 'other';
// Allowed remote authorities
['ssh-remote', 'dev-container', 'attached-container', 'wsl'].forEach((res: string) => {
if (remoteName!.indexOf(`${res}+`) === 0) {
ret = res;
}
});
return ret;
}
/**
* Retrieves the current extension based on the extension id
*/
private get extension(): vscode.Extension<any> | undefined {
if (this._extension === undefined) {
this._extension = vscode.extensions.getExtension(this.extensionId);
}
return this._extension;
}
/**
* Given an object and a callback creates a clone of the object and modifies it according to the callback
* @param obj The object to clone and modify
* @param change The modifying function
* @returns A new changed object
*/
private cloneAndChange(obj?: { [key: string]: string }, change?: (key: string, val: string) => string): { [key: string]: string } | undefined {
if (obj === null || typeof obj !== 'object') return obj;
if (typeof change !== 'function') return obj;
const ret: { [key: string]: string } = {};
for (const key in obj) {
ret[key] = change(key, obj[key]);
}
return ret;
}
/**
* Whether or not it is safe to send error telemetry
*/
private shouldSendErrorTelemetry(): boolean {
if (this.firstParty) {
if (this.cleanRemoteName(vscode.env.remoteName) !== 'other') {
return true;
}
if (this.extension === undefined || this.extension.extensionKind === vscode.ExtensionKind.Workspace) {
return false;
}
if (vscode.env.uiKind === vscode.UIKind.Web) {
return false;
}
return true;
}
return true;
}
// __GDPR__COMMON__ "common.os" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.platformversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.extname" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.extversion" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.vscodemachineid" : { "endPoint": "MacAddressHash", "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.vscodesessionid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.vscodeversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.isnewappinstall" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
private getCommonProperties(): { [key: string]: string } {
const commonProperties = Object.create(null);
commonProperties['common.os'] = this.osShim.platform;
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;
switch (vscode.env.uiKind) {
case vscode.UIKind.Web:
commonProperties['common.uikind'] = 'web';
break;
case vscode.UIKind.Desktop:
commonProperties['common.uikind'] = 'desktop';
break;
default:
commonProperties['common.uikind'] = 'unknown';
}
commonProperties['common.remotename'] = this.cleanRemoteName(vscode.env.remoteName);
}
return commonProperties;
}
/**
* Given an error stack cleans up the file paths within
* @param stack The stack to clean
* @param anonymizeFilePaths Whether or not to clean the file paths or anonymize them as well
* @returns The cleaned stack
*/
private anonymizeFilePaths(stack?: string, anonymizeFilePaths?: boolean): string {
if (stack === undefined || stack === null) {
return '';
}
const cleanupPatterns = [new RegExp(vscode.env.appRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')];
if (this.extension) {
cleanupPatterns.push(new RegExp(this.extension.extensionPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'));
}
let updatedStack = stack;
if (anonymizeFilePaths) {
const cleanUpIndexes: [number, number][] = [];
for (let regexp of cleanupPatterns) {
while (true) {
const result = regexp.exec(stack);
if (!result) {
break;
}
cleanUpIndexes.push([result.index, regexp.lastIndex]);
}
}
const nodeModulesRegex = /^[\\\/]?(node_modules|node_modules\.asar)[\\\/]/;
const fileRegex = /(file:\/\/)?([a-zA-Z]:(\\\\|\\|\/)|(\\\\|\\|\/))?([\w-\._]+(\\\\|\\|\/))+[\w-\._]*/g;
let lastIndex = 0;
updatedStack = '';
while (true) {
const result = fileRegex.exec(stack);
if (!result) {
break;
}
// Anoynimize user file paths that do not need to be retained or cleaned up.
if (!nodeModulesRegex.test(result[0]) && cleanUpIndexes.every(([x, y]) => result.index < x || result.index >= y)) {
updatedStack += stack.substring(lastIndex, result.index) + '<REDACTED: user-file-path>';
lastIndex = fileRegex.lastIndex;
}
}
if (lastIndex < stack.length) {
updatedStack += stack.substr(lastIndex);
}
}
// sanitize with configured cleanup patterns
for (let regexp of cleanupPatterns) {
updatedStack = updatedStack.replace(regexp, '');
}
return updatedStack;
}
/**
* Given an event name, some properties, and measurements sends a teleemtry event
* @param eventName The name of the event
* @param properties The properties to send with the event
* @param measurements The measurements (numeric values) to send with the event
*/
public sendTelemetryEvent(eventName: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number }): void {
if (this.userOptIn && eventName !== '') {
properties = {...properties, ...this.getCommonProperties()};
const cleanProperties = this.cloneAndChange(properties, (_key: string, prop: string) => this.anonymizeFilePaths(prop, this.firstParty));
this.telemetryAppender.logEvent(`${this.extensionId}/${eventName}`, { properties: cleanProperties, measurements: measurements });
}
}
/**
* Given an event name, some properties, and measurements sends an error event
* @param eventName The name of the event
* @param properties The properties to send with the event
* @param measurements The measurements (numeric values) to send with the event
* @param errorProps If not present then we assume all properties belong to the error prop and will be anonymized
*/
public sendTelemetryErrorEvent(eventName: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number }, errorProps?: string[]): void {
if (this.userOptIn && eventName !== '') {
// always clean the properties if first party
// do not send any error properties if we shouldn't send error telemetry
// if we have no errorProps, assume all are error props
properties = {...properties, ...this.getCommonProperties()};
const cleanProperties = this.cloneAndChange(properties, (key: string, prop: string) => {
if (this.shouldSendErrorTelemetry()) {
return this.anonymizeFilePaths(prop, this.firstParty)
}
if (errorProps === undefined || errorProps.indexOf(key) !== -1) {
return 'REDACTED';
}
return this.anonymizeFilePaths(prop, this.firstParty);
});
this.telemetryAppender.logEvent(`${this.extensionId}/${eventName}`, { properties: cleanProperties, measurements: measurements });
}
}
/**
* Given an error, properties, and measurements. Sends an exception event
* @param error The error to send
* @param properties The properties to send with the event
* @param measurements The measurements (numeric values) to send with the event
*/
public sendTelemetryException(error: Error, properties?: { [key: string]: string }, measurements?: { [key: string]: number }): void {
if (this.shouldSendErrorTelemetry() && this.userOptIn && error) {
properties = {...properties, ...this.getCommonProperties()};
const cleanProperties = this.cloneAndChange(properties, (_key: string, prop: string) => this.anonymizeFilePaths(prop, this.firstParty));
this.telemetryAppender.logException(error, { properties: cleanProperties, measurements: measurements });
}
}
/**
* Disposes of the telemetry reporter
*/
public dispose(): Promise<any> {
this.telemetryAppender.flush();
return this.optOutListener.dispose();
}
}

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

@ -0,0 +1,81 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
import * as os from 'os';
import * as vscode from 'vscode';
import * as appInsights from 'applicationinsights';
import { BaseTelemtryReporter, ITelemetryAppender } from '../common/baseTelemetryReporter';
class AppInsightsAppender implements ITelemetryAppender {
private _appInsightsClient: appInsights.TelemetryClient | undefined;
constructor(key: string) {
//check if another instance is already initialized
if (appInsights.defaultClient) {
this._appInsightsClient = new appInsights.TelemetryClient(key);
// no other way to enable offline mode
this._appInsightsClient.channel.setUseDiskRetryCaching(true);
} else {
appInsights.setup(key)
.setAutoCollectRequests(false)
.setAutoCollectPerformance(false)
.setAutoCollectExceptions(false)
.setAutoCollectDependencies(false)
.setAutoDependencyCorrelation(false)
.setAutoCollectConsole(false)
.setUseDiskRetryCaching(true)
.start();
this._appInsightsClient = appInsights.defaultClient;
}
if (vscode && vscode.env) {
this._appInsightsClient.context.tags[this._appInsightsClient.context.keys.userId] = vscode.env.machineId;
this._appInsightsClient.context.tags[this._appInsightsClient.context.keys.sessionId] = vscode.env.sessionId;
}
//check if it's an Asimov key to change the endpoint
if (key && key.indexOf('AIF-') === 0) {
this._appInsightsClient.config.endpointUrl = "https://vortex.data.microsoft.com/collect/v1";
}
}
logEvent(eventName: string, data?: any): void {
if (!this._appInsightsClient) {
return;
}
this._appInsightsClient.trackEvent({
name: eventName,
properties: data.properties,
measurements: data.measurements
});
}
logException(exception: Error, data?: any): void {
if (!this._appInsightsClient) {
return;
}
this._appInsightsClient.trackException({
exception,
properties: data.properties,
measurements: data.measurements
});
}
flush(): Promise<void> {
if (this._appInsightsClient) {
this._appInsightsClient.flush();
this._appInsightsClient = undefined;
}
return Promise.resolve(undefined);
}
}
export default class TelemetryReporter extends BaseTelemtryReporter {
constructor(extensionId: string, extensionVersion: string, key: string, firstParty?: boolean) {
const appender = new AppInsightsAppender(key);
if (key && key.indexOf('AIF-') === 0) {
firstParty = true;
}
super(extensionId, extensionVersion, appender, { release: os.release(), platform: os.platform() }, firstParty);
}
}

12
src/node/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["ES2015"],
"target": "ES6",
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "./lib",
"typeRoots": []
},
"include": [".", "../common"]
}

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

@ -1,329 +0,0 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
'use strict';
(process.env['APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL'] as any) = true;
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import * as appInsights from 'applicationinsights';
export default class TelemetryReporter {
private appInsightsClient: appInsights.TelemetryClient | undefined;
private firstParty: boolean = false;
private userOptIn: boolean = false;
private _extension: vscode.Extension<any> | undefined;
private readonly optOutListener: vscode.Disposable;
private static TELEMETRY_CONFIG_ID = 'telemetry';
private static TELEMETRY_CONFIG_ENABLED_ID = 'enableTelemetry';
private logStream: fs.WriteStream | undefined;
// tslint:disable-next-line
constructor(private extensionId: string, private extensionVersion: string, key: string, firstParty?: boolean) {
this.firstParty = !!firstParty;
let logFilePath = process.env['VSCODE_LOGS'] || '';
if (logFilePath && extensionId && process.env['VSCODE_LOG_LEVEL'] === 'trace') {
logFilePath = path.join(logFilePath, `${extensionId}.txt`);
this.logStream = fs.createWriteStream(logFilePath, { flags: 'a', encoding: 'utf8', autoClose: true });
}
this.updateUserOptIn(key);
if (vscode.env.onDidChangeTelemetryEnabled !== undefined) {
this.optOutListener = vscode.env.onDidChangeTelemetryEnabled(() => this.updateUserOptIn(key));
} else {
this.optOutListener = vscode.workspace.onDidChangeConfiguration(() => this.updateUserOptIn(key));
}
}
private updateUserOptIn(key: string): void {
// Newer versions of vscode api have telemetry enablement exposed, but fallback to setting for older versions
const config = vscode.workspace.getConfiguration(TelemetryReporter.TELEMETRY_CONFIG_ID);
const newOptInValue = vscode.env.isTelemetryEnabled === undefined ?
config.get<boolean>(TelemetryReporter.TELEMETRY_CONFIG_ENABLED_ID, true) :
vscode.env.isTelemetryEnabled;
if (this.userOptIn !== newOptInValue) {
this.userOptIn = newOptInValue;
if (this.userOptIn) {
this.createAppInsightsClient(key);
} else {
this.dispose();
}
}
}
private createAppInsightsClient(key: string) {
//check if another instance is already initialized
if (appInsights.defaultClient) {
this.appInsightsClient = new appInsights.TelemetryClient(key);
// no other way to enable offline mode
this.appInsightsClient.channel.setUseDiskRetryCaching(true);
} else {
appInsights.setup(key)
.setAutoCollectRequests(false)
.setAutoCollectPerformance(false)
.setAutoCollectExceptions(false)
.setAutoCollectDependencies(false)
.setAutoDependencyCorrelation(false)
.setAutoCollectConsole(false)
.setUseDiskRetryCaching(true)
.start();
this.appInsightsClient = appInsights.defaultClient;
}
this.appInsightsClient.commonProperties = this.getCommonProperties();
if (vscode && vscode.env) {
this.appInsightsClient.context.tags[this.appInsightsClient.context.keys.userId] = vscode.env.machineId;
this.appInsightsClient.context.tags[this.appInsightsClient.context.keys.sessionId] = vscode.env.sessionId;
}
//check if it's an Asimov key to change the endpoint
if (key && key.indexOf('AIF-') === 0) {
this.appInsightsClient.config.endpointUrl = "https://vortex.data.microsoft.com/collect/v1";
this.firstParty = true;
}
}
// __GDPR__COMMON__ "common.os" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.platformversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.extname" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.extversion" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.vscodemachineid" : { "endPoint": "MacAddressHash", "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.vscodesessionid" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.vscodeversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.uikind" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.remotename" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// __GDPR__COMMON__ "common.isnewappinstall" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
private getCommonProperties(): { [key: string]: string } {
const commonProperties = Object.create(null);
commonProperties['common.os'] = os.platform();
commonProperties['common.platformversion'] = (os.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;
switch (vscode.env.uiKind) {
case vscode.UIKind.Web:
commonProperties['common.uikind'] = 'web';
break;
case vscode.UIKind.Desktop:
commonProperties['common.uikind'] = 'desktop';
break;
default:
commonProperties['common.uikind'] = 'unknown';
}
commonProperties['common.remotename'] = this.cleanRemoteName(vscode.env.remoteName);
}
return commonProperties;
}
private cleanRemoteName(remoteName?: string): string {
if (!remoteName) {
return 'none';
}
let ret = 'other';
// Allowed remote authorities
['ssh-remote', 'dev-container', 'attached-container', 'wsl'].forEach((res: string) => {
if (remoteName!.indexOf(`${res}+`) === 0) {
ret = res;
}
});
return ret;
}
private shouldSendErrorTelemetry(): boolean {
if (this.firstParty) {
if (this.cleanRemoteName(vscode.env.remoteName) !== 'other') {
return true;
}
if (this.extension === undefined || this.extension.extensionKind === vscode.ExtensionKind.Workspace) {
return false;
}
if (vscode.env.uiKind === vscode.UIKind.Web) {
return false;
}
return true;
}
return true;
}
private get extension(): vscode.Extension<any> | undefined {
if (this._extension === undefined) {
this._extension = vscode.extensions.getExtension(this.extensionId);
}
return this._extension;
}
private cloneAndChange(obj?: { [key: string]: string }, change?: (key: string, val: string) => string): { [key: string]: string } | undefined {
if (obj === null || typeof obj !== 'object') return obj;
if (typeof change !== 'function') return obj;
const ret: { [key: string ]: string } = {};
for (const key in obj) {
ret[key] = change(key, obj[key]);
}
return ret;
}
private anonymizeFilePaths(stack?: string, anonymizeFilePaths?: boolean): string {
if (stack === undefined || stack === null) {
return '';
}
const cleanupPatterns = [new RegExp(vscode.env.appRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')];
if (this.extension) {
cleanupPatterns.push(new RegExp(this.extension.extensionPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'));
}
let updatedStack = stack;
if (anonymizeFilePaths) {
const cleanUpIndexes: [number, number][] = [];
for (let regexp of cleanupPatterns) {
while (true) {
const result = regexp.exec(stack);
if (!result) {
break;
}
cleanUpIndexes.push([result.index, regexp.lastIndex]);
}
}
const nodeModulesRegex = /^[\\\/]?(node_modules|node_modules\.asar)[\\\/]/;
const fileRegex = /(file:\/\/)?([a-zA-Z]:(\\\\|\\|\/)|(\\\\|\\|\/))?([\w-\._]+(\\\\|\\|\/))+[\w-\._]*/g;
let lastIndex = 0;
updatedStack = '';
while (true) {
const result = fileRegex.exec(stack);
if (!result) {
break;
}
// Anoynimize user file paths that do not need to be retained or cleaned up.
if (!nodeModulesRegex.test(result[0]) && cleanUpIndexes.every(([x, y]) => result.index < x || result.index >= y)) {
updatedStack += stack.substring(lastIndex, result.index) + '<REDACTED: user-file-path>';
lastIndex = fileRegex.lastIndex;
}
}
if (lastIndex < stack.length) {
updatedStack += stack.substr(lastIndex);
}
}
// sanitize with configured cleanup patterns
for (let regexp of cleanupPatterns) {
updatedStack = updatedStack.replace(regexp, '');
}
return updatedStack;
}
public sendTelemetryEvent(eventName: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number }): void {
if (this.userOptIn && eventName && this.appInsightsClient) {
const cleanProperties = this.cloneAndChange(properties, (_key: string, prop: string) => this.anonymizeFilePaths(prop, this.firstParty));
this.appInsightsClient.trackEvent({
name: `${this.extensionId}/${eventName}`,
properties: cleanProperties,
measurements: measurements
})
if (this.logStream) {
this.logStream.write(`telemetry/${eventName} ${JSON.stringify({ properties, measurements })}\n`);
}
}
}
public sendTelemetryErrorEvent(eventName: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number }, errorProps?: string[]): void {
if (this.userOptIn && eventName && this.appInsightsClient) {
// always clean the properties if first party
// do not send any error properties if we shouldn't send error telemetry
// if we have no errorProps, assume all are error props
const cleanProperties = this.cloneAndChange(properties, (key: string, prop: string) => {
if (this.shouldSendErrorTelemetry()) {
return this.anonymizeFilePaths(prop, this.firstParty)
}
if (errorProps === undefined || errorProps.indexOf(key) !== -1) {
return 'REDACTED';
}
return this.anonymizeFilePaths(prop, this.firstParty);
});
this.appInsightsClient.trackEvent({
name: `${this.extensionId}/${eventName}`,
properties: cleanProperties,
measurements: measurements
})
if (this.logStream) {
this.logStream.write(`telemetry/${eventName} ${JSON.stringify({ properties, measurements })}\n`);
}
}
}
public sendTelemetryException(error: Error, properties?: { [key: string]: string }, measurements?: { [key: string]: number }): void {
if (this.shouldSendErrorTelemetry() && this.userOptIn && error && this.appInsightsClient) {
const cleanProperties = this.cloneAndChange(properties, (_key: string, prop: string) => this.anonymizeFilePaths(prop, this.firstParty));
this.appInsightsClient.trackException({
exception: error,
properties: cleanProperties,
measurements: measurements
})
if (this.logStream) {
this.logStream.write(`telemetry/${error.name} ${error.message} ${JSON.stringify({ properties, measurements })}\n`);
}
}
}
public dispose(): Promise<any> {
this.optOutListener.dispose();
const flushEventsToLogger = new Promise<any>(resolve => {
if (!this.logStream) {
return resolve(void 0);
}
this.logStream.on('finish', resolve);
this.logStream.end();
});
const flushEventsToAI = new Promise<any>(resolve => {
if (this.appInsightsClient) {
this.appInsightsClient.flush({
callback: () => {
// all data flushed
this.appInsightsClient = undefined;
resolve(void 0);
}
});
} else {
resolve(void 0);
}
});
return Promise.all([flushEventsToAI, flushEventsToLogger]);
}
}

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

@ -1,25 +1,13 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"noEmitOnError": false,
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true,
"declaration": true,
"stripInternal": true,
"outDir": "./lib",
"strictNullChecks": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"inlineSources": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"lib": [
"es2015"
]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
"noUncheckedIndexedAccess": true
}
}