Initial scaffolding for a web version of the module
This commit is contained in:
Родитель
260c7c3a5a
Коммит
bad27f4eca
|
@ -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
|
|
@ -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))
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
*.d.ts
|
||||
*.js
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -6,5 +6,4 @@
|
|||
"**/.DS_Store": true,
|
||||
"**/*.js": {"when": "$(basename).ts"}
|
||||
},
|
||||
"eslint.enable": false
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
15
package.json
15
package.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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче