388 lines
17 KiB
JavaScript
388 lines
17 KiB
JavaScript
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||
|
// Licensed under the MIT license.
|
||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||
|
});
|
||
|
};
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
exports.OfficeAddinUsageData = exports.ExpectedError = exports.UsageDataLevel = exports.UsageDataReportingMethod = void 0;
|
||
|
const appInsights = require("applicationinsights");
|
||
|
const readLine = require("readline-sync");
|
||
|
const jsonData = require("./usageDataSettings");
|
||
|
const defaults = require("./defaults");
|
||
|
/* global process */
|
||
|
/**
|
||
|
* Specifies the usage data infrastructure the user wishes to use
|
||
|
* @enum Application Insights: Microsoft Azure service used to collect and query through data
|
||
|
*/
|
||
|
var UsageDataReportingMethod;
|
||
|
(function (UsageDataReportingMethod) {
|
||
|
UsageDataReportingMethod["applicationInsights"] = "applicationInsights";
|
||
|
})(UsageDataReportingMethod = exports.UsageDataReportingMethod || (exports.UsageDataReportingMethod = {}));
|
||
|
/**
|
||
|
* Level controlling what type of usage data is being sent
|
||
|
* @enum off: off level of usage data, sends no usage data
|
||
|
* @enum on: on level of usage data, sends errors and events
|
||
|
*/
|
||
|
var UsageDataLevel;
|
||
|
(function (UsageDataLevel) {
|
||
|
UsageDataLevel["off"] = "off";
|
||
|
UsageDataLevel["on"] = "on";
|
||
|
})(UsageDataLevel = exports.UsageDataLevel || (exports.UsageDataLevel = {}));
|
||
|
/**
|
||
|
* Defines an error that is expected to happen given some situation
|
||
|
* @member message Message to be logged in the error
|
||
|
*/
|
||
|
class ExpectedError extends Error {
|
||
|
constructor(message) {
|
||
|
super(message);
|
||
|
// need to adjust the prototype after super()
|
||
|
// See https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||
|
Object.setPrototypeOf(this, ExpectedError.prototype);
|
||
|
}
|
||
|
}
|
||
|
exports.ExpectedError = ExpectedError;
|
||
|
/**
|
||
|
* Creates and initializes member variables while prompting user for usage data collection when necessary
|
||
|
* @param usageDataObject
|
||
|
*/
|
||
|
class OfficeAddinUsageData {
|
||
|
constructor(usageDataOptions) {
|
||
|
this.usageDataClient = appInsights.defaultClient;
|
||
|
this.eventsSent = 0;
|
||
|
this.exceptionsSent = 0;
|
||
|
this.defaultData = {
|
||
|
Platform: process.platform,
|
||
|
NodeVersion: process.version,
|
||
|
};
|
||
|
try {
|
||
|
this.options = Object.assign({ groupName: defaults.groupName, promptQuestion: "", raisePrompt: true, usageDataLevel: UsageDataLevel.off, method: UsageDataReportingMethod.applicationInsights, isForTesting: false }, usageDataOptions);
|
||
|
if (this.options.instrumentationKey === undefined) {
|
||
|
throw new Error("Instrumentation Key not defined - cannot create usage data object");
|
||
|
}
|
||
|
if (this.options.groupName === undefined) {
|
||
|
throw new Error("Group Name not defined - cannot create usage data object");
|
||
|
}
|
||
|
if (jsonData.groupNameExists(this.options.groupName)) {
|
||
|
this.options.usageDataLevel = jsonData.readUsageDataLevel(this.options.groupName);
|
||
|
}
|
||
|
// Generator-office will not raise a prompt because the yeoman generator creates the prompt. If the projectName
|
||
|
// is defaults.generatorOffice and a office-addin-usage-data file hasn't been written yet, write one out.
|
||
|
if (this.options.projectName === defaults.generatorOffice &&
|
||
|
this.options.instrumentationKey === defaults.instrumentationKeyForOfficeAddinCLITools &&
|
||
|
jsonData.needToPromptForUsageData(this.options.groupName)) {
|
||
|
jsonData.writeUsageDataJsonData(this.options.groupName, this.options.usageDataLevel);
|
||
|
}
|
||
|
if (!this.options.isForTesting &&
|
||
|
this.options.raisePrompt &&
|
||
|
jsonData.needToPromptForUsageData(this.options.groupName)) {
|
||
|
this.usageDataOptIn();
|
||
|
}
|
||
|
this.options.deviceID = jsonData.readDeviceID();
|
||
|
if (this.options.usageDataLevel === UsageDataLevel.on) {
|
||
|
appInsights.setup(this.options.instrumentationKey).setAutoCollectExceptions(false).start();
|
||
|
this.usageDataClient = appInsights.defaultClient;
|
||
|
this.removeApplicationInsightsSensitiveInformation();
|
||
|
}
|
||
|
}
|
||
|
catch (err) {
|
||
|
throw new Error(err);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Reports custom event object to usage data structure
|
||
|
* @param eventName Event name sent to usage data structure
|
||
|
* @param data Data object sent to usage data structure
|
||
|
*/
|
||
|
reportEvent(eventName, data) {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
if (this.getUsageDataLevel() === UsageDataLevel.on) {
|
||
|
this.reportEventApplicationInsights(eventName, data);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* Reports custom event object to Application Insights
|
||
|
* @param eventName Event name sent to Application Insights
|
||
|
* @param data Data object sent to Application Insights
|
||
|
*/
|
||
|
reportEventApplicationInsights(eventName, data) {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
if (this.getUsageDataLevel() === UsageDataLevel.on) {
|
||
|
const usageDataEvent = new appInsights.Contracts.EventData();
|
||
|
usageDataEvent.name = this.options.isForTesting ? `${eventName}-test` : eventName;
|
||
|
try {
|
||
|
for (const [key, [value, elapsedTime]] of Object.entries(data)) {
|
||
|
usageDataEvent.properties[key] = value;
|
||
|
usageDataEvent.measurements[key + " durationElapsed"] = elapsedTime;
|
||
|
}
|
||
|
usageDataEvent.properties["deviceID"] = this.options.deviceID;
|
||
|
this.usageDataClient.trackEvent(usageDataEvent);
|
||
|
this.eventsSent++;
|
||
|
}
|
||
|
catch (err) {
|
||
|
this.reportError("sendUsageDataEvents", err);
|
||
|
throw new Error(err);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* Reports error to usage data structure
|
||
|
* @param errorName Error name sent to usage data structure
|
||
|
* @param err Error sent to usage data structure
|
||
|
*/
|
||
|
reportError(errorName, err) {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
if (this.getUsageDataLevel() === UsageDataLevel.on) {
|
||
|
this.reportErrorApplicationInsights(errorName, err);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* Reports error to Application Insights
|
||
|
* @param errorName Error name sent to Application Insights
|
||
|
* @param err Error sent to Application Insights
|
||
|
*/
|
||
|
reportErrorApplicationInsights(errorName, err) {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
if (this.getUsageDataLevel() === UsageDataLevel.on) {
|
||
|
let error = Object.create(err);
|
||
|
error.name = this.options.isForTesting ? `${errorName}-test` : errorName;
|
||
|
this.usageDataClient.trackException({
|
||
|
exception: this.maskFilePaths(error),
|
||
|
});
|
||
|
this.exceptionsSent++;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* Prompts user for usage data participation once and records response
|
||
|
* @param testData Specifies whether test code is calling this method
|
||
|
* @param testReponse Specifies test response
|
||
|
*/
|
||
|
usageDataOptIn(testData = this.options.isForTesting, testResponse = "") {
|
||
|
try {
|
||
|
let response = "";
|
||
|
if (testData) {
|
||
|
response = testResponse;
|
||
|
}
|
||
|
else {
|
||
|
response = readLine.question(`${this.options.promptQuestion}\n`);
|
||
|
}
|
||
|
if (response.toLowerCase() === "y") {
|
||
|
this.options.usageDataLevel = UsageDataLevel.on;
|
||
|
}
|
||
|
else {
|
||
|
this.options.usageDataLevel = UsageDataLevel.off;
|
||
|
}
|
||
|
jsonData.writeUsageDataJsonData(this.options.groupName, this.options.usageDataLevel);
|
||
|
}
|
||
|
catch (err) {
|
||
|
this.reportError("UsageDataOptIn", err);
|
||
|
throw new Error(err);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Stops usage data from being sent, by default usage data will be on
|
||
|
*/
|
||
|
setUsageDataOff() {
|
||
|
appInsights.defaultClient.config.samplingPercentage = 0;
|
||
|
}
|
||
|
/**
|
||
|
* Starts sending usage data, by default usage data will be on
|
||
|
*/
|
||
|
setUsageDataOn() {
|
||
|
appInsights.defaultClient.config.samplingPercentage = 100;
|
||
|
}
|
||
|
/**
|
||
|
* Returns whether the usage data is currently on or off
|
||
|
* @returns Whether usage data is turned on or off
|
||
|
*/
|
||
|
isUsageDataOn() {
|
||
|
return appInsights.defaultClient.config.samplingPercentage === 100;
|
||
|
}
|
||
|
/**
|
||
|
* Returns the instrumentation key associated with the resource
|
||
|
* @returns The usage data instrumentation key
|
||
|
*/
|
||
|
getUsageDataKey() {
|
||
|
return this.options.instrumentationKey;
|
||
|
}
|
||
|
/**
|
||
|
* Transform the project name by adddin '-test' suffix to it if necessary
|
||
|
*/
|
||
|
getEventName() {
|
||
|
return this.options.isForTesting ? `${this.options.projectName}-test` : this.options.projectName;
|
||
|
}
|
||
|
/**
|
||
|
* Returns the amount of events that have been sent
|
||
|
* @returns The count of events sent
|
||
|
*/
|
||
|
getEventsSent() {
|
||
|
return this.eventsSent;
|
||
|
}
|
||
|
/**
|
||
|
* Returns the amount of exceptions that have been sent
|
||
|
* @returns The count of exceptions sent
|
||
|
*/
|
||
|
getExceptionsSent() {
|
||
|
return this.exceptionsSent;
|
||
|
}
|
||
|
/**
|
||
|
* Get the usage data level
|
||
|
* @returns the usage data level
|
||
|
*/
|
||
|
getUsageDataLevel() {
|
||
|
return this.options.usageDataLevel;
|
||
|
}
|
||
|
/**
|
||
|
* Returns parsed file path, scrubbing file names and sensitive information
|
||
|
* @returns Error after removing PII
|
||
|
*/
|
||
|
maskFilePaths(err) {
|
||
|
try {
|
||
|
const regexRemoveUserFilePaths = /(\w:)*[/\\](.*[/\\]+)*(.+\.)+[a-zA-Z]+/gim;
|
||
|
const maskToken = "<filepath>";
|
||
|
err.message = err.message.replace(regexRemoveUserFilePaths, maskToken);
|
||
|
err.stack = err.stack.replace(regexRemoveUserFilePaths, maskToken);
|
||
|
return err;
|
||
|
}
|
||
|
catch (err) {
|
||
|
this.reportError("maskFilePaths", err);
|
||
|
throw new Error(err);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Removes sensitive information fields from ApplicationInsights data
|
||
|
*/
|
||
|
removeApplicationInsightsSensitiveInformation() {
|
||
|
delete this.usageDataClient.context.tags["ai.cloud.roleInstance"]; // cloud name
|
||
|
delete this.usageDataClient.context.tags["ai.device.id"]; // machine name
|
||
|
delete this.usageDataClient.context.tags["ai.user.accountId"]; // subscription
|
||
|
}
|
||
|
/**
|
||
|
* Reports custom exception event object to Application Insights
|
||
|
* @param method Method name sent to Application Insights
|
||
|
* @param err Error or message about error sent to Application Insights
|
||
|
* @param data Data object(s) sent to Application Insights
|
||
|
*/
|
||
|
reportException(method, err, data = {}) {
|
||
|
if (this.getUsageDataLevel() === UsageDataLevel.on) {
|
||
|
try {
|
||
|
if (err instanceof ExpectedError) {
|
||
|
this.reportExpectedException(method, err, data);
|
||
|
return;
|
||
|
}
|
||
|
let error = err instanceof Error ? Object.create(err) : new Error(`${this.options.projectName} error: ${err}`);
|
||
|
error.name = this.getEventName();
|
||
|
let exceptionTelemetryObj = {
|
||
|
exception: this.maskFilePaths(error),
|
||
|
properties: {},
|
||
|
};
|
||
|
Object.entries(Object.assign(Object.assign(Object.assign({ Succeeded: false, Method: method, ExpectedError: false }, this.defaultData), data), { deviceID: this.options.deviceID })).forEach((entry) => {
|
||
|
exceptionTelemetryObj.properties[entry[0]] = JSON.stringify(entry[1]);
|
||
|
});
|
||
|
this.usageDataClient.trackException(exceptionTelemetryObj);
|
||
|
this.exceptionsSent++;
|
||
|
}
|
||
|
catch (e) {
|
||
|
this.reportError("reportException", e);
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Reports custom expected exception event object to Application Insights
|
||
|
* @param method Method name sent to Application Insights
|
||
|
* @param err Error or message about error sent to Application Insights
|
||
|
* @param data Data object(s) sent to Application Insights
|
||
|
*/
|
||
|
reportExpectedException(method, err, data = {}) {
|
||
|
let error = err instanceof Error ? Object.create(err) : new Error(`${this.options.projectName} error: ${err}`);
|
||
|
error.name = this.getEventName();
|
||
|
this.maskFilePaths(error);
|
||
|
const errorMessage = error instanceof Error ? error.message : error;
|
||
|
this.sendUsageDataEvent(Object.assign({ Succeeded: true, Method: method, ExpectedError: true, Error: errorMessage }, data));
|
||
|
}
|
||
|
/**
|
||
|
* Reports custom success event object to Application Insights
|
||
|
* @param method Method name sent to Application Insights
|
||
|
* @param data Data object(s) sent to Application Insights
|
||
|
*/
|
||
|
reportSuccess(method, data = {}) {
|
||
|
this.sendUsageDataEvent(Object.assign({ Succeeded: true, Method: method, ExpectedError: false }, data));
|
||
|
}
|
||
|
/**
|
||
|
* Reports custom exception event object to Application Insights
|
||
|
* @param method Method name sent to Application Insights
|
||
|
* @param err Error or message about error sent to Application Insights
|
||
|
* @param data Data object(s) sent to Application Insights
|
||
|
* @deprecated Use `reportUnexpectedError` instead.
|
||
|
*/
|
||
|
sendUsageDataException(method, err, data = {}) {
|
||
|
if (this.getUsageDataLevel() === UsageDataLevel.on) {
|
||
|
try {
|
||
|
let error = err instanceof Error ? Object.create(err) : new Error(`${this.options.projectName} error: ${err}`);
|
||
|
error.name = this.getEventName();
|
||
|
let exceptionTelemetryObj = {
|
||
|
exception: this.maskFilePaths(error),
|
||
|
properties: {},
|
||
|
};
|
||
|
Object.entries(Object.assign(Object.assign({ Succeeded: false, Method: method }, this.defaultData), data)).forEach((entry) => {
|
||
|
exceptionTelemetryObj.properties[entry[0]] = JSON.stringify(entry[1]);
|
||
|
});
|
||
|
this.usageDataClient.trackException(exceptionTelemetryObj);
|
||
|
this.exceptionsSent++;
|
||
|
}
|
||
|
catch (e) {
|
||
|
this.reportError("sendUsageDataException", e);
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Reports custom success event object to Application Insights
|
||
|
* @param method Method name sent to Application Insights
|
||
|
* @param data Data object(s) sent to Application Insights
|
||
|
* @deprecated Use `reportSuccess` instead.
|
||
|
*/
|
||
|
sendUsageDataSuccessEvent(method, data = {}) {
|
||
|
this.sendUsageDataEvent(Object.assign({ Succeeded: true, Method: method, Pass: true }, data));
|
||
|
}
|
||
|
/**
|
||
|
* Reports custom successful fail event object to Application Insights
|
||
|
* "Successful fail" means that there was an error as a result of user error, but our code worked properly
|
||
|
* @param method Method name sent to Application Insights
|
||
|
* @param data Data object(s) sent to Application Insights
|
||
|
* @deprecated Use `reportExpectedError` instead.
|
||
|
*/
|
||
|
sendUsageDataSuccessfulFailEvent(method, data = {}) {
|
||
|
this.sendUsageDataEvent(Object.assign({ Succeeded: true, Method: method, Pass: false }, data));
|
||
|
}
|
||
|
/**
|
||
|
* Reports custom event object to Application Insights
|
||
|
* @param data Data object(s) sent to Application Insights
|
||
|
*/
|
||
|
sendUsageDataEvent(data = {}) {
|
||
|
if (this.getUsageDataLevel() === UsageDataLevel.on) {
|
||
|
try {
|
||
|
let eventTelemetryObj = new appInsights.Contracts.EventData();
|
||
|
eventTelemetryObj.name = this.getEventName();
|
||
|
eventTelemetryObj.properties = Object.assign(Object.assign(Object.assign({}, this.defaultData), data), { deviceID: this.options.deviceID });
|
||
|
this.usageDataClient.trackEvent(eventTelemetryObj);
|
||
|
this.eventsSent++;
|
||
|
}
|
||
|
catch (e) {
|
||
|
this.reportError("sendUsageDataEvent", e);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
exports.OfficeAddinUsageData = OfficeAddinUsageData;
|
||
|
//# sourceMappingURL=usageData.js.map
|