// 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 = ""; 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