From 0dc07e4fb4624fb411b52bbc7eebe36cc78e7613 Mon Sep 17 00:00:00 2001 From: Rosen Vladimirov Date: Mon, 19 Nov 2018 23:24:16 +0200 Subject: [PATCH 1/5] fix(analytics): remove tracking to Eqatec Analytics Remove tracking to Eqatec Analytics as we no longer use it. --- lib/bootstrap.ts | 1 - lib/commands/debug.ts | 1 - lib/common/commands/analytics.ts | 4 +- lib/common/commands/proxy/proxy-base.ts | 5 +- lib/common/declarations.d.ts | 16 +- lib/common/definitions/cli-global.d.ts | 5 - lib/common/definitions/eqatec-analytics.d.ts | 35 -- lib/common/definitions/eqatec.d.ts | 28 -- lib/common/services/analytics-service-base.ts | 198 +-------- lib/common/services/commands-service.ts | 1 - .../test/unit-tests/analytics-service.ts | 396 +----------------- lib/common/vendor/EqatecMonitor.min.js | 30 -- lib/definitions/platform.d.ts | 16 - lib/helpers/livesync-command-helper.ts | 1 - lib/services/analytics/analytics-broker.ts | 35 +- lib/services/analytics/analytics-constants.ts | 16 +- lib/services/analytics/analytics-service.ts | 13 +- lib/services/analytics/analytics.d.ts | 53 +-- .../analytics/eqatec-analytics-provider.ts | 61 --- lib/services/doctor-service.ts | 3 +- lib/services/livesync/livesync-service.ts | 21 - lib/services/platform-service.ts | 34 +- lib/services/project-templates-service.ts | 2 - test/commands/post-install.ts | 2 +- test/stubs.ts | 8 - 25 files changed, 42 insertions(+), 943 deletions(-) delete mode 100644 lib/common/definitions/eqatec-analytics.d.ts delete mode 100644 lib/common/definitions/eqatec.d.ts delete mode 100644 lib/common/vendor/EqatecMonitor.min.js delete mode 100644 lib/services/analytics/eqatec-analytics-provider.ts diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 54f9cfa4ac..4477d677b1 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -35,7 +35,6 @@ $injector.require("androidDebugService", "./services/android-debug-service"); $injector.require("userSettingsService", "./services/user-settings-service"); $injector.requirePublic("analyticsSettingsService", "./services/analytics-settings-service"); $injector.require("analyticsService", "./services/analytics/analytics-service"); -$injector.require("eqatecAnalyticsProvider", "./services/analytics/eqatec-analytics-provider"); $injector.require("googleAnalyticsProvider", "./services/analytics/google-analytics-provider"); $injector.require("emulatorSettingsService", "./services/emulator-settings-service"); diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index 0129c33f05..3869b2640b 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -34,7 +34,6 @@ export class DebugPlatformCommand extends ValidatePlatformCommandBase implements const debugOptions = _.cloneDeep(this.$options.argv); - await this.$platformService.trackProjectType(this.$projectData); const selectedDeviceForDebug = await this.getDeviceForDebug(); const debugData = this.$debugDataService.createDebugData(this.$projectData, {device: selectedDeviceForDebug.deviceInfo.identifier}); diff --git a/lib/common/commands/analytics.ts b/lib/common/commands/analytics.ts index 3735c346c6..7f01a8110b 100644 --- a/lib/common/commands/analytics.ts +++ b/lib/common/commands/analytics.ts @@ -31,11 +31,11 @@ class AnalyticsCommand implements ICommand { switch (arg.toLowerCase()) { case "enable": await this.$analyticsService.setStatus(this.settingName, true); - await this.$analyticsService.track(this.settingName, "enabled"); + // await this.$analyticsService.track(this.settingName, "enabled"); this.$logger.info(`${this.humanReadableSettingName} is now enabled.`); break; case "disable": - await this.$analyticsService.track(this.settingName, "disabled"); + // await this.$analyticsService.track(this.settingName, "disabled"); await this.$analyticsService.setStatus(this.settingName, false); this.$logger.info(`${this.humanReadableSettingName} is now disabled.`); break; diff --git a/lib/common/commands/proxy/proxy-base.ts b/lib/common/commands/proxy/proxy-base.ts index ecd448dc11..e6be6cdd21 100644 --- a/lib/common/commands/proxy/proxy-base.ts +++ b/lib/common/commands/proxy/proxy-base.ts @@ -12,7 +12,10 @@ export abstract class ProxyCommandBase implements ICommand { protected async tryTrackUsage() { try { - await this.$analyticsService.trackFeature(this.commandName); + // TODO: Check why we have set the `disableAnalytics` to true and we track the command as separate one + // instead of tracking it through the commandsService. + this.$logger.trace(this.commandName); + // await this.$analyticsService.trackFeature(this.commandName); } catch (ex) { this.$logger.trace("Error in trying to track proxy command usage:"); this.$logger.trace(ex); diff --git a/lib/common/declarations.d.ts b/lib/common/declarations.d.ts index 7035deb61f..bd7bb95b7f 100644 --- a/lib/common/declarations.d.ts +++ b/lib/common/declarations.d.ts @@ -186,11 +186,6 @@ declare const enum TrackingTypes { */ Initialization = "initialization", - /** - * Defines that the data contains feature that should be tracked. - */ - Feature = "feature", - /** * Defines that the data contains exception that should be tracked. */ @@ -670,19 +665,10 @@ interface IDictionary { interface IAnalyticsService { checkConsent(): Promise; - trackFeature(featureName: string): Promise; trackException(exception: any, message: string): Promise; setStatus(settingName: string, enabled: boolean): Promise; getStatusMessage(settingName: string, jsonFormat: boolean, readableSettingName: string): Promise; isEnabled(settingName: string): Promise; - track(featureName: string, featureValue: string): Promise; - - /** - * Tries to stop current eqatec monitor, clean it's state and remove the process.exit event handler. - * @param {string|number} code - Exit code as the method is used for process.exit event handler. - * @return void - */ - tryStopEqatecMonitors(code?: string | number): void; /** * Tracks the answer of question if user allows to be tracked. @@ -2035,4 +2021,4 @@ declare module "stringify-package" { declare module "detect-newline" { function detectNewline(data: string): string | null; export = detectNewline -} \ No newline at end of file +} diff --git a/lib/common/definitions/cli-global.d.ts b/lib/common/definitions/cli-global.d.ts index 80d691ecbf..368029f58d 100644 --- a/lib/common/definitions/cli-global.d.ts +++ b/lib/common/definitions/cli-global.d.ts @@ -7,11 +7,6 @@ interface ICliGlobal extends NodeJS.Global { */ _: any; - /** - * Eqatec analytics instance. - */ - _eqatec: IEqatec; - /** * Global instance of the module used for dependency injection. */ diff --git a/lib/common/definitions/eqatec-analytics.d.ts b/lib/common/definitions/eqatec-analytics.d.ts deleted file mode 100644 index 8880a99dbf..0000000000 --- a/lib/common/definitions/eqatec-analytics.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Describes the information that will be passed to analytics for tracking. - */ -interface ITrackingInformation { - /** - * The type of the data sent to analytics service - initalization data, feature to be tracked, error to be tracked, etc. - */ - type: TrackingTypes -} - -/** - * Describes information required for starting Eqatec Analytics tracking. - */ -interface IEqatecInitializeData extends ITrackingInformation { - /** - * The API key of the project in which we'll collect data. - */ - analyticsAPIKey: string; - - /** - * The number of the current session in this project for this user. In case the value is 1, Analytics will count the user as new one. - * Whenever a new session is started, we should increase the count. - */ - userSessionCount: number; - - /** - * The installation identifier of analytics. It is unique per user. - */ - analyticsInstallationId: string; - - /** - * The unique identifier of a user. - */ - userId: string; -} diff --git a/lib/common/definitions/eqatec.d.ts b/lib/common/definitions/eqatec.d.ts deleted file mode 100644 index 033ba41a4c..0000000000 --- a/lib/common/definitions/eqatec.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -interface IEqatecLogging { - logMessage(...args: string[]): void; - logError(...args: string[]): void; -} - -interface IEqatecSettings { - useHttps: boolean; - userAgent: string; - version: string; - useCookies: boolean; - loggingInterface: IEqatecLogging; -} - -interface IEqatecMonitor { - trackFeature(featureNameAndValue: string): void; - trackException(exception: Error, message: string): void; - stop(): void; - setInstallationID(guid: string): void; - setUserID(userId: string): void; - setStartCount(count: number): void; - start(): void; - status(): { isSending: boolean }; -} - -interface IEqatec { - createSettings(analyticsProjectKey: string): IEqatecSettings; - createMonitor(settings: IEqatecSettings): IEqatecMonitor; -} \ No newline at end of file diff --git a/lib/common/services/analytics-service-base.ts b/lib/common/services/analytics-service-base.ts index bf962a70d6..06c4818267 100644 --- a/lib/common/services/analytics-service-base.ts +++ b/lib/common/services/analytics-service-base.ts @@ -1,16 +1,7 @@ import * as helpers from "../helpers"; -import { AnalyticsClients } from "../constants"; import { cache } from "../decorators"; -const cliGlobal = global; -// HACK -cliGlobal.XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; -cliGlobal.XMLHttpRequest.prototype.withCredentials = false; -// HACK -end - export abstract class AnalyticsServiceBase implements IAnalyticsService, IDisposable { - private static MAX_WAIT_SENDING_INTERVAL = 30000; // in milliseconds - protected eqatecMonitors: IDictionary = {}; protected featureTrackingAPIKeys: string[] = [ this.$staticConfig.ANALYTICS_API_KEY ]; @@ -33,8 +24,7 @@ export abstract class AnalyticsServiceBase implements IAnalyticsService, IDispos protected $processService: IProcessService, private $prompter: IPrompter, private $userSettingsService: UserSettings.IUserSettingsService, - private $analyticsSettingsService: IAnalyticsSettingsService, - private $osInfo: IOsInfo) { } + private $analyticsSettingsService: IAnalyticsSettingsService) { } protected get acceptTrackFeatureSetting(): string { return `Accept${this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME}`; @@ -73,43 +63,11 @@ export abstract class AnalyticsServiceBase implements IAnalyticsService, IDispos } public async trackAcceptFeatureUsage(settings: { acceptTrackFeatureUsage: boolean }): Promise { - try { - await this.sendDataToEqatecMonitors(this.acceptUsageReportingAPIKeys, - (eqatecMonitor: IEqatecMonitor) => eqatecMonitor.trackFeature(`${this.acceptTrackFeatureSetting}.${settings.acceptTrackFeatureUsage}`)); - } catch (e) { - this.$logger.trace("Analytics exception: ", e); - } - } - - public trackFeature(featureName: string): Promise { - const category = this.$options.analyticsClient || - (helpers.isInteractive() ? AnalyticsClients.Cli : AnalyticsClients.NonInteractive); - return this.track(category, featureName); - } - - public async track(featureName: string, featureValue: string): Promise { - await this.initAnalyticsStatuses(); - this.$logger.trace(`Trying to track feature '${featureName}' with value '${featureValue}'.`); - - if (this.analyticsStatuses[this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME] === AnalyticsStatus.enabled) { - await this.trackFeatureCore(`${featureName}.${featureValue}`); - } + // Void at the moment } public async trackException(exception: any, message: string): Promise { - await this.initAnalyticsStatuses(); - - if (this.analyticsStatuses[this.$staticConfig.ERROR_REPORT_SETTING_NAME] === AnalyticsStatus.enabled - && await this.$analyticsSettingsService.canDoRequest()) { - try { - this.$logger.trace(`Trying to track exception with message '${message}'.`); - - await this.sendDataToEqatecMonitors(this.exceptionsTrackingAPIKeys, - (eqatecMonitor: IEqatecMonitor) => eqatecMonitor.trackException(exception, message)); - } catch (e) { - this.$logger.trace("Analytics exception: ", e); - } - } + // void } public async trackInGoogleAnalytics(gaSettings: IGoogleAnalyticsData): Promise { @@ -123,11 +81,6 @@ export abstract class AnalyticsServiceBase implements IAnalyticsService, IDispos public async setStatus(settingName: string, enabled: boolean): Promise { this.analyticsStatuses[settingName] = enabled ? AnalyticsStatus.enabled : AnalyticsStatus.disabled; await this.$userSettingsService.saveSetting(settingName, enabled.toString()); - - if (this.analyticsStatuses[settingName] === AnalyticsStatus.disabled - && this.analyticsStatuses[settingName] === AnalyticsStatus.disabled) { - this.tryStopEqatecMonitors(); - } } public async isEnabled(settingName: string): Promise { @@ -135,14 +88,6 @@ export abstract class AnalyticsServiceBase implements IAnalyticsService, IDispos return analyticsStatus === AnalyticsStatus.enabled; } - public tryStopEqatecMonitors(code?: string | number): void { - for (const eqatecMonitorApiKey in this.eqatecMonitors) { - const eqatecMonitor = this.eqatecMonitors[eqatecMonitorApiKey]; - eqatecMonitor.stop(); - delete this.eqatecMonitors[eqatecMonitorApiKey]; - } - } - public getStatusMessage(settingName: string, jsonFormat: boolean, readableSettingName: string): Promise { if (jsonFormat) { return this.getJsonStatusMessage(settingName); @@ -151,16 +96,6 @@ export abstract class AnalyticsServiceBase implements IAnalyticsService, IDispos return this.getHumanReadableStatusMessage(settingName, readableSettingName); } - protected async trackFeatureCore(featureTrackString: string, settings?: { userInteraction: boolean }): Promise { - try { - if (await this.$analyticsSettingsService.canDoRequest()) { - await this.sendDataToEqatecMonitors(this.featureTrackingAPIKeys, (eqatecMonitor: IEqatecMonitor) => eqatecMonitor.trackFeature(featureTrackString)); - } - } catch (e) { - this.$logger.trace("Analytics exception: ", e); - } - } - @cache() protected async initAnalyticsStatuses(): Promise { if (await this.$analyticsSettingsService.canDoRequest()) { @@ -175,58 +110,6 @@ export abstract class AnalyticsServiceBase implements IAnalyticsService, IDispos } } - private getIsSending(eqatecMonitor: IEqatecMonitor): boolean { - return eqatecMonitor.status().isSending; - } - - private waitForSending(eqatecMonitor: IEqatecMonitor): Promise { - return new Promise((resolve, reject) => { - const intervalTime = 100; - let remainingTime = AnalyticsServiceBase.MAX_WAIT_SENDING_INTERVAL; - - if (this.getIsSending(eqatecMonitor)) { - const message = `Waiting for analytics to send information. Will check in ${intervalTime}ms.`; - this.$logger.trace(message); - const interval = setInterval(() => { - if (!this.getIsSending(eqatecMonitor) || remainingTime <= 0) { - clearInterval(interval); - resolve(); - } - - remainingTime -= intervalTime; - this.$logger.trace(`${message} Remaining time is: ${remainingTime}`); - }, intervalTime); - } else { - resolve(); - } - }); - } - - private async sendDataToEqatecMonitors(analyticsAPIKeys: string[], eqatecMonitorAction: (eqatecMonitor: IEqatecMonitor) => void): Promise { - for (const eqatecAPIKey of analyticsAPIKeys) { - const eqatecMonitor = await this.start(eqatecAPIKey); - eqatecMonitorAction(eqatecMonitor); - await this.waitForSending(eqatecMonitor); - } - } - - private async getCurrentSessionCount(analyticsProjectKey: string): Promise { - let currentCount = await this.$analyticsSettingsService.getUserSessionsCount(analyticsProjectKey); - await this.$analyticsSettingsService.setUserSessionsCount(++currentCount, analyticsProjectKey); - - return currentCount; - } - - private async getEqatecSettings(analyticsAPIKey: string): Promise { - return { - analyticsAPIKey, - analyticsInstallationId: await this.$analyticsSettingsService.getClientId(), - type: TrackingTypes.Initialization, - userId: await this.$analyticsSettingsService.getUserId(), - userSessionCount: await this.getCurrentSessionCount(analyticsAPIKey) - }; - } - private async getStatus(settingName: string): Promise { if (!_.has(this.analyticsStatuses, settingName)) { const settingValue = await this.$userSettingsService.getSettingValue(settingName); @@ -246,81 +129,6 @@ export abstract class AnalyticsServiceBase implements IAnalyticsService, IDispos return this.analyticsStatuses[settingName]; } - private async start(analyticsAPIKey: string): Promise { - const eqatecMonitorForSpecifiedAPIKey = this.eqatecMonitors[analyticsAPIKey]; - if (eqatecMonitorForSpecifiedAPIKey) { - return eqatecMonitorForSpecifiedAPIKey; - } - - const analyticsSettings = await this.getEqatecSettings(analyticsAPIKey); - return this.startEqatecMonitor(analyticsSettings); - } - - private async startEqatecMonitor(analyticsSettings: IEqatecInitializeData): Promise { - const eqatecMonitorForSpecifiedAPIKey = this.eqatecMonitors[analyticsSettings.analyticsAPIKey]; - if (eqatecMonitorForSpecifiedAPIKey) { - return eqatecMonitorForSpecifiedAPIKey; - } - - require("../vendor/EqatecMonitor.min"); - const analyticsProjectKey = analyticsSettings.analyticsAPIKey; - const settings = cliGlobal._eqatec.createSettings(analyticsProjectKey); - settings.useHttps = false; - settings.userAgent = this.getUserAgentString(); - settings.version = this.$staticConfig.version; - settings.useCookies = false; - settings.loggingInterface = { - logMessage: this.$logger.trace.bind(this.$logger), - logError: this.$logger.debug.bind(this.$logger) - }; - - const eqatecMonitor = cliGlobal._eqatec.createMonitor(settings); - this.eqatecMonitors[analyticsSettings.analyticsAPIKey] = eqatecMonitor; - - const analyticsInstallationId = analyticsSettings.analyticsInstallationId; - - this.$logger.trace(`${this.$staticConfig.ANALYTICS_INSTALLATION_ID_SETTING_NAME}: ${analyticsInstallationId}`); - eqatecMonitor.setInstallationID(analyticsInstallationId); - - try { - await eqatecMonitor.setUserID(analyticsSettings.userId); - - const currentCount = analyticsSettings.userSessionCount; - eqatecMonitor.setStartCount(currentCount); - } catch (e) { - // user not logged in. don't care. - this.$logger.trace("Error while initializing eqatecMonitor", e); - } - - eqatecMonitor.start(); - - // End the session on process.exit only or in case user disables both usage tracking and exceptions tracking. - this.$processService.attachToProcessExitSignals(this, this.tryStopEqatecMonitors); - - await this.reportNodeVersion(analyticsSettings.analyticsAPIKey); - - return eqatecMonitor; - } - - private async reportNodeVersion(apiKey: string): Promise { - const reportedVersion: string = process.version.slice(1).replace(/[.]/g, "_"); - await this.sendDataToEqatecMonitors([apiKey], (eqatecMonitor: IEqatecMonitor) => eqatecMonitor.trackFeature(`NodeJSVersion.${reportedVersion}`)); - } - - private getUserAgentString(): string { - let userAgentString: string; - const osType = this.$osInfo.type(); - if (osType === "Windows_NT") { - userAgentString = "(Windows NT " + this.$osInfo.release() + ")"; - } else if (osType === "Darwin") { - userAgentString = "(Mac OS X " + this.$osInfo.release() + ")"; - } else { - userAgentString = "(" + osType + ")"; - } - - return userAgentString; - } - private async isNotConfirmed(settingName: string): Promise { const analyticsStatus = await this.getStatus(settingName); return analyticsStatus === AnalyticsStatus.notConfirmed; diff --git a/lib/common/services/commands-service.ts b/lib/common/services/commands-service.ts index 19c44ab0f2..3a84a7c23e 100644 --- a/lib/common/services/commands-service.ts +++ b/lib/common/services/commands-service.ts @@ -45,7 +45,6 @@ export class CommandsService implements ICommandsService { if (!this.$staticConfig.disableAnalytics && !command.disableAnalytics) { const analyticsService = this.$injector.resolve("analyticsService"); // This should be resolved here due to cyclic dependency await analyticsService.checkConsent(); - await analyticsService.trackFeature(commandName); const beautifiedCommandName = this.beautifyCommandName(commandName).replace(/\|/g, " "); diff --git a/lib/common/test/unit-tests/analytics-service.ts b/lib/common/test/unit-tests/analytics-service.ts index ac659fe11c..e00c197122 100644 --- a/lib/common/test/unit-tests/analytics-service.ts +++ b/lib/common/test/unit-tests/analytics-service.ts @@ -4,78 +4,10 @@ import { AnalyticsServiceBase } from '../../services/analytics-service-base'; import helpersLib = require("../../helpers"); import { HostInfo } from "../../host-info"; import { OsInfo } from "../../os-info"; -require("../../vendor/EqatecMonitor.min"); // note - it modifies global scope! const assert = require("chai").assert; -const cliGlobal = global; -const originalEqatec = cliGlobal._eqatec; -const originalIsInteractive = helpersLib.isInteractive; -const exception = "Exception"; -const message = "Track Exception Msg"; -let trackedFeatureNamesAndValues: string[] = []; +const originalIsInteractive = helpersLib.isInteractive; let savedSettingNamesAndValues = ""; -let trackedExceptionMessages: string[] = []; -let lastUsedEqatecSettings: IEqatecSettings; -let usedEqatecSettings: IEqatecSettings[] = []; -let apiKeysPassedToCreateEqatecSettings: string[] = []; -let isEqatecStopCalled = false; -let startCalledCount = 0; - -class AnalyticsServiceInheritor extends AnalyticsServiceBase { - public featureTrackingAPIKeys: string[] = [ - this.$staticConfig.ANALYTICS_API_KEY - ]; - - public acceptUsageReportingAPIKeys: string[] = [ - this.$staticConfig.ANALYTICS_API_KEY - ]; - - public exceptionsTrackingAPIKeys: string[] = [ - this.$staticConfig.ANALYTICS_EXCEPTIONS_API_KEY - ]; - - public dispose(): void { - // nothing to do here - } -} - -function setGlobalEqatec(shouldSetUserThrowException: boolean, shouldStartThrow: boolean): void { - cliGlobal._eqatec = { - createSettings: (apiKey: string) => { - apiKeysPassedToCreateEqatecSettings.push(apiKey); - return {}; - }, - createMonitor: (settings: any) => { - lastUsedEqatecSettings = settings; - usedEqatecSettings.push(settings); - - return { - trackFeature: (featureNameAndValue: string) => { - trackedFeatureNamesAndValues.push(featureNameAndValue); - }, - trackException: (exceptionToTrack: any, messageToTrack: string) => { - trackedExceptionMessages.push(messageToTrack); - }, - stop: () => { isEqatecStopCalled = true; }, - setInstallationID: (guid: string) => { /*a mock*/ }, - setUserID: (userId: string) => { - if (shouldSetUserThrowException) { - throw new Error("setUserID throws"); - } - }, - start: () => { - startCalledCount++; - if (shouldStartThrow) { - throw new Error("start throws"); - } - }, - setStartCount: (count: number) => { /*a mock */ }, - status: () => ({ isSending: false }) - }; - }, - - }; -} class UserSettingsServiceStub { constructor(public featureTracking: boolean, @@ -110,8 +42,6 @@ interface ITestScenario { } function createTestInjector(testScenario: ITestScenario): IInjector { - setGlobalEqatec(testScenario.shouldSetUserThrowException, testScenario.shouldStartThrow); - const testInjector = new Yok(); testInjector.register("logger", CommonLoggerStub); testInjector.register("errors", ErrorsStub); @@ -167,7 +97,6 @@ function createTestInjector(testScenario: ITestScenario): IInjector { describe("analytics-service-base", () => { let baseTestScenario: ITestScenario; - const featureName = "unit tests feature"; let service: IAnalyticsService = null; beforeEach(() => { @@ -180,156 +109,15 @@ describe("analytics-service-base", () => { shouldSetUserThrowException: false, shouldStartThrow: false }; - trackedFeatureNamesAndValues = []; - trackedExceptionMessages = []; savedSettingNamesAndValues = ""; - isEqatecStopCalled = false; - lastUsedEqatecSettings = {}; - usedEqatecSettings = []; - apiKeysPassedToCreateEqatecSettings = []; service = null; - startCalledCount = 0; - }); - - afterEach(() => { - // clean up the process.exit event handler - if (service) { - service.tryStopEqatecMonitors(); - } }); after(() => { - cliGlobal._eqatec = originalEqatec; helpersLib.isInteractive = originalIsInteractive; }); - describe("trackFeature", () => { - it("tracks feature when console is interactive", async () => { - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.trackFeature(featureName); - assert.isTrue(trackedFeatureNamesAndValues.indexOf(`CLI.${featureName}`) !== -1); - service.tryStopEqatecMonitors(); - }); - - it("tracks feature when console is not interactive", async () => { - baseTestScenario.isInteractive = false; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.trackFeature(featureName); - assert.isTrue(trackedFeatureNamesAndValues.indexOf(`Non-interactive.${featureName}`) !== -1); - service.tryStopEqatecMonitors(); - }); - - it("does not track feature when console is interactive and feature tracking is disabled", async () => { - baseTestScenario.featureTracking = false; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.trackFeature(featureName); - assert.deepEqual(trackedFeatureNamesAndValues, []); - }); - - it("does not track feature when console is not interactive and feature tracking is disabled", async () => { - baseTestScenario.featureTracking = baseTestScenario.isInteractive = false; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.trackFeature(featureName); - assert.deepEqual(trackedFeatureNamesAndValues, []); - }); - - it("does not track feature when console is interactive and feature tracking is enabled, but cannot make request", async () => { - baseTestScenario.canDoRequest = false; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.trackFeature(featureName); - assert.deepEqual(trackedFeatureNamesAndValues, []); - }); - - it("does not track feature when console is not interactive and feature tracking is enabled, but cannot make request", async () => { - baseTestScenario.canDoRequest = baseTestScenario.isInteractive = false; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.trackFeature(featureName); - assert.deepEqual(trackedFeatureNamesAndValues, []); - }); - - it("does not throw exception when eqatec start throws", async () => { - baseTestScenario.shouldStartThrow = true; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.trackFeature(featureName); - assert.deepEqual(trackedFeatureNamesAndValues, []); - }); - }); - - describe("trackException", () => { - it("tracks when all conditions are correct", async () => { - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.trackException(exception, message); - assert.isTrue(trackedExceptionMessages.indexOf(message) !== -1); - }); - - it("does not track when exception tracking is disabled", async () => { - baseTestScenario.exceptionsTracking = false; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.trackException(exception, message); - assert.deepEqual(trackedExceptionMessages, []); - }); - - it("does not track when feature tracking is enabled, but cannot make request", async () => { - baseTestScenario.canDoRequest = false; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.trackException(exception, message); - assert.deepEqual(trackedExceptionMessages, []); - }); - - it("does not throw exception when eqatec start throws", async () => { - baseTestScenario.shouldStartThrow = true; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.trackException(exception, message); - assert.deepEqual(trackedExceptionMessages, []); - }); - }); - - describe("track", () => { - const name = "unitTests"; - it("tracks when all conditions are correct", async () => { - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.track(name, featureName); - assert.isTrue(trackedFeatureNamesAndValues.indexOf(`${name}.${featureName}`) !== -1); - }); - - it("does not track when feature tracking is disabled", async () => { - baseTestScenario.featureTracking = false; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.track(name, featureName); - assert.deepEqual(trackedFeatureNamesAndValues, []); - }); - - it("does not track when feature tracking is enabled, but cannot make request", async () => { - baseTestScenario.canDoRequest = false; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.track(name, featureName); - assert.deepEqual(trackedFeatureNamesAndValues, []); - }); - - it("does not throw exception when eqatec start throws", async () => { - baseTestScenario.shouldStartThrow = true; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.track(name, featureName); - assert.deepEqual(trackedFeatureNamesAndValues, []); - }); - }); - describe("isEnabled", () => { it("returns true when analytics status is enabled", async () => { const testInjector = createTestInjector(baseTestScenario); @@ -366,20 +154,6 @@ describe("analytics-service-base", () => { await service.setStatus(staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME, false); assert.isTrue(savedSettingNamesAndValues.indexOf(`${staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME}.false`) !== -1); }); - - it("calls eqatec stop when all analytics trackings are disabled", async () => { - baseTestScenario.exceptionsTracking = false; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - const staticConfig: Config.IStaticConfig = testInjector.resolve("staticConfig"); - // start eqatec - await service.trackFeature(featureName); - assert.isTrue(trackedFeatureNamesAndValues.indexOf(`CLI.${featureName}`) !== -1); - await service.setStatus(staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME, false); - assert.isTrue(savedSettingNamesAndValues.indexOf(`${staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME}.false`) !== -1); - assert.isTrue(isEqatecStopCalled); - }); - }); describe("getStatusMessage", () => { @@ -515,173 +289,5 @@ describe("analytics-service-base", () => { await service.checkConsent(); assert.deepEqual(savedSettingNamesAndValues, ""); }); - - it("sends information that user had accepted feature tracking", async () => { - baseTestScenario.featureTracking = undefined; - baseTestScenario.exceptionsTracking = false; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.checkConsent(); - const staticConfig: Config.IStaticConfig = testInjector.resolve("staticConfig"); - assert.isTrue(trackedFeatureNamesAndValues.indexOf(`Accept${staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME}.true`) !== -1); - }); - - it("sends information that user had rejected feature tracking", async () => { - baseTestScenario.featureTracking = undefined; - baseTestScenario.prompterConfirmResult = false; - baseTestScenario.exceptionsTracking = false; - const testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - await service.checkConsent(); - const staticConfig: Config.IStaticConfig = testInjector.resolve("staticConfig"); - assert.isTrue(trackedFeatureNamesAndValues.indexOf(`Accept${staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME}.false`) !== -1); - }); - }); - - describe("uses correct settings on different os-es", () => { - const name = "unitTests"; - let testInjector: IInjector; - let osInfo: IOsInfo; - let osType: () => string; - let osRelease: () => string; - const release = "1.0"; - - beforeEach(() => { - testInjector = createTestInjector(baseTestScenario); - service = testInjector.resolve("analyticsService"); - osInfo = testInjector.resolve("osInfo"); - osType = osInfo.type; - osRelease = osInfo.release; - }); - - afterEach(() => { - osInfo.type = osType; - osInfo.release = osRelease; - }); - - it("sets correct userAgent on Windows", async () => { - osInfo.type = () => { return "Windows_NT"; }; - osInfo.release = () => { return release; }; - await service.track(name, featureName); - assert.equal(lastUsedEqatecSettings.userAgent, `(Windows NT ${release})`); - }); - - it("sets correct userAgent on MacOS", async () => { - osInfo.type = () => { return "Darwin"; }; - osInfo.release = () => { return release; }; - await service.track(name, featureName); - assert.equal(lastUsedEqatecSettings.userAgent, `(Mac OS X ${release})`); - }); - - it("sets correct userAgent on other OSs", async () => { - osInfo.type = () => { return "Linux"; }; - osInfo.release = () => { return release; }; - await service.track(name, featureName); - assert.equal(lastUsedEqatecSettings.userAgent, `(Linux)`); - }); - }); - - describe("tracks to multiple projects simultaneously", () => { - let analyticsService: AnalyticsServiceInheritor; - const getNodeJsVersionString = () => { - const reportedVersion = process.version.slice(1).replace(/[.]/g, "_"); - return `NodeJSVersion.${reportedVersion}`; - }; - - it("features when featureTrackingAPIKeys setting contains multiple keys", async () => { - const testInjector = createTestInjector(baseTestScenario); - analyticsService = testInjector.resolve("analyticsService"); - analyticsService.featureTrackingAPIKeys = [ - "ApiKey1", - "ApiKey2" - ]; - - const secondFeatureName = "second feature"; - - await analyticsService.trackFeature(featureName); - await analyticsService.trackFeature(secondFeatureName); - - assert.equal(startCalledCount, 2, "When we have two API Keys, the start method should be called exactly two times."); - assert.deepEqual(apiKeysPassedToCreateEqatecSettings, analyticsService.featureTrackingAPIKeys); - - const featureFullName = `CLI.${featureName}`; - const secondFeatureFullName = `CLI.${secondFeatureName}`; - const nodeJsVersionString = getNodeJsVersionString(); - const expectedTrackedFeatures = [ - nodeJsVersionString, - featureFullName, - nodeJsVersionString, - featureFullName, - secondFeatureFullName, - secondFeatureFullName - ]; - - assert.deepEqual(trackedFeatureNamesAndValues, expectedTrackedFeatures); - }); - - it("exceptions when exceptionsTrackingAPIKeys setting contains multiple keys", async () => { - const testInjector = createTestInjector(baseTestScenario); - analyticsService = testInjector.resolve("analyticsService"); - analyticsService.exceptionsTrackingAPIKeys = [ - "ApiKey1", - "ApiKey2" - ]; - - const secondExceptionMessage = "second exception"; - - await analyticsService.trackException(exception, message); - await analyticsService.trackException(exception, secondExceptionMessage); - - assert.equal(startCalledCount, 2, "When we have two API Keys, the start method should be called exactly two times."); - assert.deepEqual(apiKeysPassedToCreateEqatecSettings, analyticsService.exceptionsTrackingAPIKeys); - - const expectedExceptionMessage = [ - message, - message, // for second project - secondExceptionMessage, - secondExceptionMessage // for second project - ]; - - assert.deepEqual(trackedExceptionMessages, expectedExceptionMessage); - - const nodeJsVersionString = getNodeJsVersionString(); - - // NodeJSVersion should be tracked in all processes for exceptions tracking. - const expectedTrackedMessages = [ - nodeJsVersionString, - nodeJsVersionString, - ]; - assert.deepEqual(trackedFeatureNamesAndValues, expectedTrackedMessages); - }); - - it("accept feature tracking when acceptUsageReportingAPIKeys setting contains multiple keys", async () => { - const testInjector = createTestInjector(baseTestScenario); - analyticsService = testInjector.resolve("analyticsService"); - analyticsService.acceptUsageReportingAPIKeys = [ - "ApiKey1", - "ApiKey2" - ]; - - await analyticsService.trackAcceptFeatureUsage({ acceptTrackFeatureUsage: true }); - await analyticsService.trackAcceptFeatureUsage({ acceptTrackFeatureUsage: false }); - - const staticConfig = testInjector.resolve("staticConfig"); - - assert.equal(startCalledCount, 2, "When we have two API Keys, the start method should be called exactly two times."); - assert.deepEqual(apiKeysPassedToCreateEqatecSettings, analyticsService.acceptUsageReportingAPIKeys); - const acceptTrackFeatureUsageTrue = `Accept${staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME}.true`; - const acceptTrackFeatureUsageFalse = `Accept${staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME}.false`; - const nodeJsVersionString = getNodeJsVersionString(); - const expectedTrackedFeatures = [ - nodeJsVersionString, - acceptTrackFeatureUsageTrue, - nodeJsVersionString, - acceptTrackFeatureUsageTrue, - acceptTrackFeatureUsageFalse, - acceptTrackFeatureUsageFalse - ]; - - assert.deepEqual(trackedFeatureNamesAndValues, expectedTrackedFeatures); - }); }); }); diff --git a/lib/common/vendor/EqatecMonitor.min.js b/lib/common/vendor/EqatecMonitor.min.js deleted file mode 100644 index fc7aa39f00..0000000000 --- a/lib/common/vendor/EqatecMonitor.min.js +++ /dev/null @@ -1,30 +0,0 @@ -(function(k){function z(b){var a=!1,f="",e=0,d=1,g={},h=[],i=[],l=function(a){if(c.isString(a)){var f=c.trim(c.asString(a));if(f){if(c.stringTooLong(b,f,1E3))return null;var a="eq_"+a,d=g[a];d||(d={featureName:f,sessionHitCount:0,timingStart:0},g[a]=d);return d}return null}};return{addFeatureUsage:function(a){if(a=l(a))a.sessionHitCount+=1,d++},startFeatureTiming:function(a){if((a=l(a))&&!(0b++;a+=51*b&52?(b^15?8^Math.random()*(b^20?16:4):4).toString(16):"-");return a},setCookie:function(b,a,f,e,d){var g=document;g&&(b=b+"="+a+";path="+f+";",e&&0f?(c.error(b,"Input is too long: "+a),!0):!1}};(function(){function b(a){return 10>a?"0"+a:a}function a(a){d.lastIndex=0;return d.test(a)?'"'+a.replace(d,function(a){var b=i[a];return"string"===typeof b?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+ -'"':'"'+a+'"'}function f(b,c){var d,e,i,k,n=g,m,j=c[b];j&&"object"===typeof j&&"function"===typeof j.toJSON&&(j=j.toJSON(b));"function"===typeof l&&(j=l.call(c,b,j));switch(typeof j){case "string":return a(j);case "number":return isFinite(j)?""+j:"null";case "boolean":case "null":return""+j;case "object":if(!j)return"null";g+=h;m=[];if("[object Array]"===Object.prototype.toString.apply(j)){k=j.length;for(d=0;d\s*\(/gm,"{anonymous}()@").split("\n")},firefox:function(a){return a.stack.replace(/(?:\n@:0)?\s+$/m,"").replace(/^\(/gm,"{anonymous}(").split("\n")}, -opera10:function(a){var a=a.stacktrace.split("\n"),b=/.*line (\d+), column (\d+) in ((/g,"{anonymous}");a[d++]=i+"@"+h}a.splice(d,a.length-d);return a},opera:function(a){var a=a.message.split("\n"),b=/Line\s+(\d+).*script\s+(http\S+)(?:.*in\s+function\s+(\S+))?/i,c,d,g;c=4; -d=0;for(g=a.length;cc.length;){g=b.test(a.toString());h=[];try{h=Array.prototype.slice.call(a.arguments),a=a.caller}catch(i){a=null}g=g?RegExp.$1||"{anonymous}":"{anonymous}";c[d++]=g+"("+this.stringifyArguments(h)+")";if(!a)break}return c}, -stringifyArguments:function(a){for(var b=0;bc.length?"["+this.stringifyArguments(c)+"]":"["+this.stringifyArguments(Array.prototype.slice.call(c,0,1))+"..."+this.stringifyArguments(Array.prototype.slice.call(c,-1))+"]":c.constructor===Object?a[b]="#object":c.constructor===Function?a[b]="#function":c.constructor===String&&(a[b]='"'+c+'"'))}return a.join(",")}};c.stackTrace=b})(); -var t=k._eqatec||{};k._eqatec=t;t.createMonitor=function(b){var a=0,f="",e=!1,d={logError:function(){},logMessage:function(){}},g=0,h=!1,i=!1,l=0,t="",r="",w="",s={},x=!1,u=!0,n=!1,m=void 0,j=!1,y="",o={};try{b=b||{},f=c.trim(c.asString(b.productId))||"",d=b.loggingInterface||d,w=b.version,s=b.location||{},x=c.cookiesEnabled()&&b.useCookies,u=b.useHttps,m=b.userAgent,n=b.testMode,j=c.trackingDisabled(),y=c.getServerUri(d,f,b.serverUri,u),o=z(d),(32>f.length||36r.length||36; - /** - * Sends information to analytics for current project type. - * The information is sent once per process for each project. - * In long living process, where the project may change, each of the projects will be tracked after it's being opened. - * @param {IProjectData} projectData DTO with information about the project. - * @returns {Promise} - */ - trackProjectType(projectData: IProjectData): Promise; - - /** - * Sends information to analytics for specific platform related action, for example Build, LiveSync, etc. - * @param {ITrackPlatformAction} actionData The data describing current action. - * @returns {Promise} - */ - trackActionForPlatform(actionData: ITrackPlatformAction): Promise; - /** * Saves build information in a proprietary file. * @param {string} platform The build platform. diff --git a/lib/helpers/livesync-command-helper.ts b/lib/helpers/livesync-command-helper.ts index 7c1597ec7a..f91463af07 100644 --- a/lib/helpers/livesync-command-helper.ts +++ b/lib/helpers/livesync-command-helper.ts @@ -178,7 +178,6 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { projectName: this.$projectData.projectName } ); - await this.$platformService.trackProjectType(this.$projectData); } } } diff --git a/lib/services/analytics/analytics-broker.ts b/lib/services/analytics/analytics-broker.ts index 25d4bf1c3f..296ba02ff0 100644 --- a/lib/services/analytics/analytics-broker.ts +++ b/lib/services/analytics/analytics-broker.ts @@ -2,47 +2,22 @@ import { cache } from "../../common/decorators"; export class AnalyticsBroker implements IAnalyticsBroker { - @cache() - private async getEqatecAnalyticsProvider(): Promise { - return this.$injector.resolve("eqatecAnalyticsProvider"); - } - @cache() private async getGoogleAnalyticsProvider(): Promise { const clientId = await this.$analyticsSettingsService.getClientId(); - return this.$injector.resolve("googleAnalyticsProvider", { clientId }); + return this.$injector.resolve("googleAnalyticsProvider", { clientId, analyticsLoggingService: this.analyticsLoggingService }); } constructor(private $analyticsSettingsService: IAnalyticsSettingsService, - private $injector: IInjector) { } + private $injector: IInjector, + private analyticsLoggingService: IAnalyticsLoggingService) { } public async sendDataForTracking(trackInfo: ITrackingInformation): Promise { try { - const eqatecProvider = await this.getEqatecAnalyticsProvider(); const googleProvider = await this.getGoogleAnalyticsProvider(); - - switch (trackInfo.type) { - case TrackingTypes.Exception: - await eqatecProvider.trackError(trackInfo); - break; - case TrackingTypes.Feature: - await eqatecProvider.trackInformation(trackInfo); - break; - case TrackingTypes.AcceptTrackFeatureUsage: - await eqatecProvider.acceptFeatureUsageTracking(trackInfo); - break; - case TrackingTypes.GoogleAnalyticsData: - await googleProvider.trackHit(trackInfo); - break; - case TrackingTypes.Finish: - await eqatecProvider.finishTracking(); - break; - default: - throw new Error(`Invalid tracking type: ${trackInfo.type}`); - } + await googleProvider.trackHit(trackInfo); } catch (err) { - // So, lets ignore the error for now until we find out what to do with it. + this.analyticsLoggingService.logData({ message: `AnalyticsBroker unable to execute action in sendDataForTracking: ${err}`, type: AnalyticsLoggingMessageType.Error }); } } - } diff --git a/lib/services/analytics/analytics-constants.ts b/lib/services/analytics/analytics-constants.ts index 5aff79435c..aabd599f5b 100644 --- a/lib/services/analytics/analytics-constants.ts +++ b/lib/services/analytics/analytics-constants.ts @@ -5,10 +5,20 @@ const enum AnalyticsMessages { /** * Analytics Broker is initialized and is ready to receive information for tracking. */ - BrokerReadyToReceive = "BrokerReadyToReceive", + BrokerReadyToReceive = "BrokerReadyToReceive" +} + +/** + * Defines the type of the messages that should be written in the local analyitcs log file (in case such is specified). + */ +const enum AnalyticsLoggingMessageType { + /** + * Information message. This is the default value in case type is not specified. + */ + Info = "Info", /** - * Eqatec Analytics process is initialized and is ready to receive information for tracking. + * Error message - used to indicate that some action while trying to track information did not succeeded. */ - EqatecAnalyticsReadyToReceive = "EqatecAnalyticsReadyToReceive" + Error = "Error" } diff --git a/lib/services/analytics/analytics-service.ts b/lib/services/analytics/analytics-service.ts index 7f34c27a97..84a9c7c4b6 100644 --- a/lib/services/analytics/analytics-service.ts +++ b/lib/services/analytics/analytics-service.ts @@ -16,21 +16,10 @@ export class AnalyticsService extends AnalyticsServiceBase { $prompter: IPrompter, $userSettingsService: UserSettings.IUserSettingsService, $analyticsSettingsService: IAnalyticsSettingsService, - $osInfo: IOsInfo, private $childProcess: IChildProcess, private $projectDataService: IProjectDataService, private $mobileHelper: Mobile.IMobileHelper) { - super($logger, $options, $staticConfig, $processService, $prompter, $userSettingsService, $analyticsSettingsService, $osInfo); - } - - public track(featureName: string, featureValue: string): Promise { - const data: IFeatureTrackingInformation = { - type: TrackingTypes.Feature, - featureName: featureName, - featureValue: featureValue - }; - - return this.sendInfoForTracking(data, this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME); + super($logger, $options, $staticConfig, $processService, $prompter, $userSettingsService, $analyticsSettingsService); } public trackException(exception: any, message: string): Promise { diff --git a/lib/services/analytics/analytics.d.ts b/lib/services/analytics/analytics.d.ts index ecb51c8b1f..97f5992a63 100644 --- a/lib/services/analytics/analytics.d.ts +++ b/lib/services/analytics/analytics.d.ts @@ -1,26 +1,21 @@ /** - * Describes if the user allows to be tracked. + * Describes the information that will be passed to analytics for tracking. */ -interface IAcceptUsageReportingInformation extends ITrackingInformation { +interface ITrackingInformation { /** - * The answer of the question if user allows us to track them. + * The type of the data sent to analytics service - initalization data, feature to be tracked, error to be tracked, etc. */ - acceptTrackFeatureUsage: boolean; + type: TrackingTypes } /** - * Describes information used for tracking feature. + * Describes if the user allows to be tracked. */ -interface IFeatureTrackingInformation extends ITrackingInformation { - /** - * The name of the feature that should be tracked. - */ - featureName: string; - +interface IAcceptUsageReportingInformation extends ITrackingInformation { /** - * Value of the feature that should be tracked. + * The answer of the question if user allows us to track them. */ - featureValue: string; + acceptTrackFeatureUsage: boolean; } /** @@ -50,38 +45,6 @@ interface IAnalyticsBroker { sendDataForTracking(trackInfo: ITrackingInformation): Promise; } -/** - * Describes analytics provider used for tracking in a specific Analytics Service. - */ -interface IAnalyticsProvider { - /** - * Sends exception for tracking in the analytics service provider. - * @param {IExceptionsTrackingInformation} trackInfo The information for exception that should be tracked. - * @returns {Promise} - */ - trackError(trackInfo: IExceptionsTrackingInformation): Promise; - - /** - * Sends feature for tracking in the analytics service provider. - * @param {IFeatureTrackingInformation} trackInfo The information for feature that should be tracked. - * @returns {Promise} - */ - trackInformation(trackInfo: IFeatureTrackingInformation): Promise; - - /** - * Sends information if user accepts to be tracked. - * @param {IAcceptUsageReportingInformation} trackInfo The information, containing user's answer if they allow to be tracked. - * @returns {Promise} - */ - acceptFeatureUsageTracking(data: IAcceptUsageReportingInformation): Promise; - - /** - * Waits for execution of all pending requests and finishes tracking operation - * @returns {Promise} - */ - finishTracking(): Promise; -} - interface IGoogleAnalyticsTrackingInformation extends IGoogleAnalyticsData, ITrackingInformation { } /** diff --git a/lib/services/analytics/eqatec-analytics-provider.ts b/lib/services/analytics/eqatec-analytics-provider.ts deleted file mode 100644 index 93278c7b32..0000000000 --- a/lib/services/analytics/eqatec-analytics-provider.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { AnalyticsServiceBase } from "../../common/services/analytics-service-base"; - -export class EqatecAnalyticsProvider extends AnalyticsServiceBase implements IAnalyticsProvider { - private static ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY = "9912cff308334c6d9ad9c33f76a983e3"; - private static NEW_PROJECT_ANALYTICS_API_KEY = "b40f24fcb4f94bccaf64e4dc6337422e"; - - protected featureTrackingAPIKeys: string[] = [ - this.$staticConfig.ANALYTICS_API_KEY, - EqatecAnalyticsProvider.NEW_PROJECT_ANALYTICS_API_KEY - ]; - - protected acceptUsageReportingAPIKeys: string[] = [ - EqatecAnalyticsProvider.ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY - ]; - - constructor(protected $logger: ILogger, - protected $options: IOptions, - $processService: IProcessService, - $staticConfig: Config.IStaticConfig, - $prompter: IPrompter, - $userSettingsService: UserSettings.IUserSettingsService, - $analyticsSettingsService: IAnalyticsSettingsService, - $osInfo: IOsInfo) { - super($logger, $options, $staticConfig, $processService, $prompter, $userSettingsService, $analyticsSettingsService, $osInfo); - } - - public async trackInformation(data: IFeatureTrackingInformation): Promise { - try { - await this.trackFeatureCore(`${data.featureName}.${data.featureValue}`); - } catch (e) { - this.$logger.trace(`Analytics exception: ${e}`); - } - } - - public async trackError(data: IExceptionsTrackingInformation): Promise { - try { - await this.trackException(data.exception, data.message); - } catch (e) { - this.$logger.trace(`Analytics exception: ${e}`); - } - } - - public async acceptFeatureUsageTracking(data: IAcceptUsageReportingInformation): Promise { - try { - await this.trackAcceptFeatureUsage({ acceptTrackFeatureUsage: data.acceptTrackFeatureUsage }); - } catch (e) { - this.$logger.trace(`Analytics exception: ${e}`); - } - } - - public async finishTracking(): Promise { - this.tryStopEqatecMonitors(); - } - - public dispose(): void { - // Intentionally left blank. - } - -} - -$injector.register("eqatecAnalyticsProvider", EqatecAnalyticsProvider); diff --git a/lib/services/doctor-service.ts b/lib/services/doctor-service.ts index 43feda6b68..79531eb513 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -31,7 +31,8 @@ class DoctorService implements IDoctorService { } if (!configOptions || configOptions.trackResult) { - await this.$analyticsService.track("DoctorEnvironmentSetup", hasWarnings ? "incorrect" : "correct"); + // TODO: Consider sending this information to Google Analytics + // await this.$analyticsService.track("DoctorEnvironmentSetup", hasWarnings ? "incorrect" : "correct"); } if (hasWarnings) { diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index ecac15210b..1da9030853 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -182,8 +182,6 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi } private async refreshApplicationWithDebug(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, debugOptions: IDebugOptions, outputPath?: string): Promise { - await this.$platformService.trackProjectType(projectData); - const deviceAppData = liveSyncResultInfo.deviceAppData; const deviceIdentifier = liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier; @@ -462,8 +460,6 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi }); } - await this.trackAction(action, platform, options); - const shouldInstall = await this.$platformService.shouldInstall(options.device, options.projectData, options, options.deviceBuildInfoDescriptor.outputPath); if (shouldInstall) { await this.$platformService.installApplication(options.device, { release: false }, options.projectData, pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath); @@ -473,22 +469,6 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi return appInstalledOnDeviceResult; } - private async trackAction(action: string, platform: string, options: IEnsureLatestAppPackageIsInstalledOnDeviceOptions): Promise { - if (!options.settings[platform][options.device.deviceInfo.type]) { - let isForDevice = !options.device.isEmulator; - options.settings[platform][options.device.deviceInfo.type] = true; - if (this.$mobileHelper.isAndroidPlatform(platform)) { - options.settings[platform][DeviceTypes.Emulator] = true; - options.settings[platform][DeviceTypes.Device] = true; - isForDevice = null; - } - - await this.$platformService.trackActionForPlatform({ action, platform, isForDevice }); - } - - await this.$platformService.trackActionForPlatform({ action: LiveSyncTrackActionNames.DEVICE_INFO, platform, isForDevice: !options.device.isEmulator, deviceOsVersion: options.device.deviceInfo.version }); - } - private async installedCachedAppPackage(platform: string, options: IEnsureLatestAppPackageIsInstalledOnDeviceOptions): Promise { const rebuildInfo = _.find(options.rebuiltInformation, info => info.platform === platform && (this.$mobileHelper.isAndroidPlatform(platform) || info.isEmulator === options.device.isEmulator)); @@ -546,7 +526,6 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi liveSyncDeviceInfo: deviceBuildInfoDescriptor }); - await this.$platformService.trackActionForPlatform({ action: "LiveSync", platform: device.deviceInfo.platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version }); await this.refreshApplication(projectData, liveSyncResultInfo, deviceBuildInfoDescriptor.debugOptions, deviceBuildInfoDescriptor.outputPath); this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`); diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index c594ba9971..d46fc27e6f 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -20,8 +20,6 @@ export class PlatformService extends EventEmitter implements IPlatformService { return this.$hooksService; } - private _trackedProjectFilePath: string = null; - constructor(private $devicesService: Mobile.IDevicesService, private $preparePlatformNativeService: IPreparePlatformService, private $preparePlatformJSService: IPreparePlatformService, @@ -294,9 +292,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { const { platform, appFilesUpdaterOptions, platformTemplate, projectData, config, nativePrepare } = preparePlatformInfo; this.validatePlatform(platform, projectData); - await this.trackProjectType(projectData); - - //We need dev-dependencies here, so before-prepare hooks will be executed correctly. + // We need dev-dependencies here, so before-prepare hooks will be executed correctly. try { await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData); } catch (err) { @@ -405,38 +401,11 @@ export class PlatformService extends EventEmitter implements IPlatformService { return prepareInfo.changesRequireBuildTime !== buildInfo.prepareTime; } - public async trackProjectType(projectData: IProjectData): Promise { - // Track each project once per process. - // In long living process, where we may work with multiple projects, we would like to track the information for each of them. - if (projectData && (projectData.projectFilePath !== this._trackedProjectFilePath)) { - this._trackedProjectFilePath = projectData.projectFilePath; - - await this.$analyticsService.track("Working with project type", projectData.projectType); - } - } - - public async trackActionForPlatform(actionData: ITrackPlatformAction): Promise { - const normalizePlatformName = this.$mobileHelper.normalizePlatformName(actionData.platform); - let featureValue = normalizePlatformName; - if (actionData.isForDevice !== null) { - const deviceType = actionData.isForDevice ? "device" : "emulator"; - featureValue += `.${deviceType}`; - } - - await this.$analyticsService.track(actionData.action, featureValue); - - if (actionData.deviceOsVersion) { - await this.$analyticsService.track(`Device OS version`, `${normalizePlatformName}_${actionData.deviceOsVersion}`); - } - } - public async buildPlatform(platform: string, buildConfig: IBuildConfig, projectData: IProjectData): Promise { this.$logger.out("Building project..."); const action = constants.TrackActionNames.Build; - await this.trackProjectType(projectData); const isForDevice = this.$mobileHelper.isAndroidPlatform(platform) ? null : buildConfig && buildConfig.buildForDevice; - await this.trackActionForPlatform({ action, platform, isForDevice }); await this.$analyticsService.trackEventActionInGoogleAnalytics({ action, @@ -591,7 +560,6 @@ export class PlatformService extends EventEmitter implements IPlatformService { this.$logger.out("Skipping install."); } - await this.trackActionForPlatform({ action: constants.TrackActionNames.Deploy, platform: device.deviceInfo.platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version }); }; if (deployInfo.deployOptions.device) { diff --git a/lib/services/project-templates-service.ts b/lib/services/project-templates-service.ts index 30a655b197..d710ee8beb 100644 --- a/lib/services/project-templates-service.ts +++ b/lib/services/project-templates-service.ts @@ -36,8 +36,6 @@ export class ProjectTemplatesService implements IProjectTemplatesService { this.$fs.deleteDirectory(path.join(templatePath, constants.NODE_MODULES_FOLDER_NAME)); } - await this.$analyticsService.track("Template used for project creation", templateValue); - const templateNameToBeTracked = this.getTemplateNameToBeTracked(templateValue, templatePackageJsonContent); if (templateNameToBeTracked) { await this.$analyticsService.trackEventActionInGoogleAnalytics({ diff --git a/test/commands/post-install.ts b/test/commands/post-install.ts index 81505cde77..78f0b04cea 100644 --- a/test/commands/post-install.ts +++ b/test/commands/post-install.ts @@ -26,7 +26,7 @@ const createTestInjector = (): IInjector => { testInjector.register("options", {}); testInjector.register("doctorService", { - printWarnings: async (configOptions?: { trackResult: boolean }): Promise => undefined + printWarnings: async (): Promise => undefined }); testInjector.register("analyticsService", { diff --git a/test/stubs.ts b/test/stubs.ts index aab3095ab6..be32216147 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -875,14 +875,6 @@ export class PlatformServiceStub extends EventEmitter implements IPlatformServic return Promise.resolve(""); } - public async trackProjectType(): Promise { - return null; - } - - public async trackActionForPlatform(actionData: ITrackPlatformAction): Promise { - return null; - } - public getCurrentPlatformVersion(platform: string, projectData: IProjectData): string { return null; } From 5626d057973cd2f47a152fcd8decc226bbda0cd1 Mon Sep 17 00:00:00 2001 From: Rosen Vladimirov Date: Tue, 20 Nov 2018 00:57:53 +0200 Subject: [PATCH 2/5] fix: remove deps to xmlhttprequest `xmlhttprequest` package has been used for Eqatec Analytics. We do not need it anymore --- npm-shrinkwrap.json | 4 ---- package.json | 1 - 2 files changed, 5 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index d3562295b3..66e0f9a66b 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -8007,10 +8007,6 @@ "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.21.tgz", "integrity": "sha1-op4SENqx8QwlZltegBKbqo1pqXs=" }, - "xmlhttprequest": { - "version": "https://github.com/telerik/node-XMLHttpRequest/tarball/master", - "integrity": "sha512-SPnuri0YNyTOxSIU/E0voUTEOEGkwI0kFuLBe9BEsbG8aIRYa9ASuizKzK3XeqnBjypF6tou5NRpuEsAW0CBxg==" - }, "xregexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", diff --git a/package.json b/package.json index 16fc03f38c..bcb78e78ae 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,6 @@ "xcode": "https://github.com/NativeScript/node-xcode/archive/NativeScript-1.5.2.tar.gz", "xml2js": "0.4.19", "xmldom": "0.1.21", - "xmlhttprequest": "https://github.com/telerik/node-XMLHttpRequest/tarball/master", "yargs": "6.0.0", "zipstream": "https://github.com/Icenium/node-zipstream/tarball/master" }, From 9e0bb64dcc274a1eacee6a402460680fa12b878c Mon Sep 17 00:00:00 2001 From: Rosen Vladimirov Date: Tue, 20 Nov 2018 00:01:05 +0200 Subject: [PATCH 3/5] refactor(analytics): remove AnalyticsServiceBase and obsolete static config props Remove `AnalyticsServiceBase` by moving the required code to `AnalyticsService`. Remove obsolete static config props. --- lib/common/definitions/config.d.ts | 2 - lib/common/services/analytics-service-base.ts | 155 ------------------ lib/common/static-config-base.ts | 2 - .../test/unit-tests/analytics-service.ts | 11 +- lib/config.ts | 2 - lib/services/analytics/analytics-service.ts | 142 +++++++++++++--- 6 files changed, 131 insertions(+), 183 deletions(-) delete mode 100644 lib/common/services/analytics-service-base.ts diff --git a/lib/common/definitions/config.d.ts b/lib/common/definitions/config.d.ts index eb92b74925..7d69012b0d 100644 --- a/lib/common/definitions/config.d.ts +++ b/lib/common/definitions/config.d.ts @@ -6,8 +6,6 @@ declare module Config { USER_AGENT_NAME: string; CLIENT_NAME_ALIAS?: string; FULL_CLIENT_NAME?: string; - ANALYTICS_API_KEY: string; - ANALYTICS_EXCEPTIONS_API_KEY: string; ANALYTICS_INSTALLATION_ID_SETTING_NAME: string; TRACK_FEATURE_USAGE_SETTING_NAME: string; ERROR_REPORT_SETTING_NAME: string; diff --git a/lib/common/services/analytics-service-base.ts b/lib/common/services/analytics-service-base.ts deleted file mode 100644 index 06c4818267..0000000000 --- a/lib/common/services/analytics-service-base.ts +++ /dev/null @@ -1,155 +0,0 @@ -import * as helpers from "../helpers"; -import { cache } from "../decorators"; - -export abstract class AnalyticsServiceBase implements IAnalyticsService, IDisposable { - protected featureTrackingAPIKeys: string[] = [ - this.$staticConfig.ANALYTICS_API_KEY - ]; - - protected acceptUsageReportingAPIKeys: string[] = [ - this.$staticConfig.ANALYTICS_API_KEY - ]; - - protected exceptionsTrackingAPIKeys: string[] = [ - this.$staticConfig.ANALYTICS_EXCEPTIONS_API_KEY - ]; - - protected shouldDisposeInstance: boolean = true; - - protected analyticsStatuses: IDictionary = {}; - - constructor(protected $logger: ILogger, - protected $options: IOptions, - protected $staticConfig: Config.IStaticConfig, - protected $processService: IProcessService, - private $prompter: IPrompter, - private $userSettingsService: UserSettings.IUserSettingsService, - private $analyticsSettingsService: IAnalyticsSettingsService) { } - - protected get acceptTrackFeatureSetting(): string { - return `Accept${this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME}`; - } - - public setShouldDispose(shouldDispose: boolean): void { - this.shouldDisposeInstance = shouldDispose; - } - - public abstract dispose(): void; - - public async checkConsent(): Promise { - if (await this.$analyticsSettingsService.canDoRequest()) { - const initialTrackFeatureUsageStatus = await this.getStatus(this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME); - let trackFeatureUsage = initialTrackFeatureUsageStatus === AnalyticsStatus.enabled; - - if (await this.isNotConfirmed(this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME) && helpers.isInteractive()) { - this.$logger.out("Do you want to help us improve " - + this.$analyticsSettingsService.getClientName() - + " by automatically sending anonymous usage statistics? We will not use this information to identify or contact you." - + " You can read our official Privacy Policy at"); - - const message = this.$analyticsSettingsService.getPrivacyPolicyLink(); - trackFeatureUsage = await this.$prompter.confirm(message, () => true); - await this.setStatus(this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME, trackFeatureUsage); - - await this.trackAcceptFeatureUsage({ acceptTrackFeatureUsage: trackFeatureUsage }); - } - - const isErrorReportingUnset = await this.isNotConfirmed(this.$staticConfig.ERROR_REPORT_SETTING_NAME); - const isUsageReportingConfirmed = !await this.isNotConfirmed(this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME); - if (isErrorReportingUnset && isUsageReportingConfirmed) { - await this.setStatus(this.$staticConfig.ERROR_REPORT_SETTING_NAME, trackFeatureUsage); - } - } - } - - public async trackAcceptFeatureUsage(settings: { acceptTrackFeatureUsage: boolean }): Promise { - // Void at the moment - } - - public async trackException(exception: any, message: string): Promise { - // void - } - - public async trackInGoogleAnalytics(gaSettings: IGoogleAnalyticsData): Promise { - // Intentionally left blank. - } - - public async trackEventActionInGoogleAnalytics(data: IEventActionData): Promise { - // Intentionally left blank. - } - - public async setStatus(settingName: string, enabled: boolean): Promise { - this.analyticsStatuses[settingName] = enabled ? AnalyticsStatus.enabled : AnalyticsStatus.disabled; - await this.$userSettingsService.saveSetting(settingName, enabled.toString()); - } - - public async isEnabled(settingName: string): Promise { - const analyticsStatus = await this.getStatus(settingName); - return analyticsStatus === AnalyticsStatus.enabled; - } - - public getStatusMessage(settingName: string, jsonFormat: boolean, readableSettingName: string): Promise { - if (jsonFormat) { - return this.getJsonStatusMessage(settingName); - } - - return this.getHumanReadableStatusMessage(settingName, readableSettingName); - } - - @cache() - protected async initAnalyticsStatuses(): Promise { - if (await this.$analyticsSettingsService.canDoRequest()) { - this.$logger.trace("Initializing analytics statuses."); - const settingsNames = [this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME, this.$staticConfig.ERROR_REPORT_SETTING_NAME]; - - for (const settingName of settingsNames) { - await this.getStatus(settingName); - } - - this.$logger.trace("Analytics statuses: ", this.analyticsStatuses); - } - } - - private async getStatus(settingName: string): Promise { - if (!_.has(this.analyticsStatuses, settingName)) { - const settingValue = await this.$userSettingsService.getSettingValue(settingName); - - if (settingValue) { - const isEnabled = helpers.toBoolean(settingValue); - if (isEnabled) { - this.analyticsStatuses[settingName] = AnalyticsStatus.enabled; - } else { - this.analyticsStatuses[settingName] = AnalyticsStatus.disabled; - } - } else { - this.analyticsStatuses[settingName] = AnalyticsStatus.notConfirmed; - } - } - - return this.analyticsStatuses[settingName]; - } - - private async isNotConfirmed(settingName: string): Promise { - const analyticsStatus = await this.getStatus(settingName); - return analyticsStatus === AnalyticsStatus.notConfirmed; - } - - private async getHumanReadableStatusMessage(settingName: string, readableSettingName: string): Promise { - let status: string = null; - - if (await this.isNotConfirmed(settingName)) { - status = "disabled until confirmed"; - } else { - status = await this.getStatus(settingName); - } - - return `${readableSettingName} is ${status}.`; - } - - private async getJsonStatusMessage(settingName: string): Promise { - const status = await this.getStatus(settingName); - const enabled = status === AnalyticsStatus.notConfirmed ? null : status === AnalyticsStatus.enabled; - return JSON.stringify({ enabled }); - } - -} diff --git a/lib/common/static-config-base.ts b/lib/common/static-config-base.ts index 0c297d9ecf..9f2ad357b5 100644 --- a/lib/common/static-config-base.ts +++ b/lib/common/static-config-base.ts @@ -5,8 +5,6 @@ import * as os from "os"; export abstract class StaticConfigBase implements Config.IStaticConfig { public PROJECT_FILE_NAME: string = null; public CLIENT_NAME: string = null; - public ANALYTICS_API_KEY: string = null; - public abstract ANALYTICS_EXCEPTIONS_API_KEY: string; public ANALYTICS_INSTALLATION_ID_SETTING_NAME: string = null; public TRACK_FEATURE_USAGE_SETTING_NAME: string = null; public ERROR_REPORT_SETTING_NAME: string = null; diff --git a/lib/common/test/unit-tests/analytics-service.ts b/lib/common/test/unit-tests/analytics-service.ts index e00c197122..75b544aacc 100644 --- a/lib/common/test/unit-tests/analytics-service.ts +++ b/lib/common/test/unit-tests/analytics-service.ts @@ -1,6 +1,6 @@ import { CommonLoggerStub, ErrorsStub } from "./stubs"; import { Yok } from "../../yok"; -import { AnalyticsServiceBase } from '../../services/analytics-service-base'; +import { AnalyticsService } from '../../../services/analytics/analytics-service'; import helpersLib = require("../../helpers"); import { HostInfo } from "../../host-info"; import { OsInfo } from "../../os-info"; @@ -43,9 +43,10 @@ interface ITestScenario { function createTestInjector(testScenario: ITestScenario): IInjector { const testInjector = new Yok(); + testInjector.register("logger", CommonLoggerStub); testInjector.register("errors", ErrorsStub); - testInjector.register("analyticsService", AnalyticsServiceBase); + testInjector.register("analyticsService", AnalyticsService); testInjector.register("analyticsSettingsService", { canDoRequest: () => { return Promise.resolve(testScenario.canDoRequest); @@ -92,10 +93,14 @@ function createTestInjector(testScenario: ITestScenario): IInjector { attachToProcessExitSignals: (context: any, callback: () => void): void => (undefined) }); + testInjector.register("childProcess", {}); + testInjector.register("projectDataService", {}); + testInjector.register("mobileHelper", {}); + return testInjector; } -describe("analytics-service-base", () => { +describe("analytics-service", () => { let baseTestScenario: ITestScenario; let service: IAnalyticsService = null; diff --git a/lib/config.ts b/lib/config.ts index 79bb4a8dd9..54cf35809a 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -26,8 +26,6 @@ export class StaticConfig extends StaticConfigBase implements IStaticConfig { public CLIENT_NAME_KEY_IN_PROJECT_FILE = "nativescript"; public CLIENT_NAME = "tns"; public CLIENT_NAME_ALIAS = "NativeScript"; - public ANALYTICS_API_KEY = "5752dabccfc54c4ab82aea9626b7338e"; - public ANALYTICS_EXCEPTIONS_API_KEY = "35478fe7de68431399e96212540a3d5d"; public TRACK_FEATURE_USAGE_SETTING_NAME = "TrackFeatureUsage"; public ERROR_REPORT_SETTING_NAME = "TrackExceptions"; public ANALYTICS_INSTALLATION_ID_SETTING_NAME = "AnalyticsInstallationID"; diff --git a/lib/services/analytics/analytics-service.ts b/lib/services/analytics/analytics-service.ts index 84a9c7c4b6..97d508cf66 100644 --- a/lib/services/analytics/analytics-service.ts +++ b/lib/services/analytics/analytics-service.ts @@ -1,35 +1,73 @@ -import { AnalyticsServiceBase } from "../../common/services/analytics-service-base"; import { ChildProcess } from "child_process"; import * as path from "path"; import { cache } from "../../common/decorators"; -import { isInteractive } from '../../common/helpers'; +import { isInteractive, toBoolean } from '../../common/helpers'; import { DeviceTypes, AnalyticsClients } from "../../common/constants"; -export class AnalyticsService extends AnalyticsServiceBase { +export class AnalyticsService implements IAnalyticsService, IDisposable { private static ANALYTICS_BROKER_START_TIMEOUT = 10 * 1000; private brokerProcess: ChildProcess; - - constructor(protected $logger: ILogger, - protected $options: IOptions, - protected $processService: IProcessService, - $staticConfig: Config.IStaticConfig, - $prompter: IPrompter, - $userSettingsService: UserSettings.IUserSettingsService, - $analyticsSettingsService: IAnalyticsSettingsService, + private shouldDisposeInstance: boolean = true; + private analyticsStatuses: IDictionary = {}; + + constructor(private $logger: ILogger, + private $options: IOptions, + private $processService: IProcessService, + private $staticConfig: Config.IStaticConfig, + private $prompter: IPrompter, + private $userSettingsService: UserSettings.IUserSettingsService, + private $analyticsSettingsService: IAnalyticsSettingsService, private $childProcess: IChildProcess, private $projectDataService: IProjectDataService, private $mobileHelper: Mobile.IMobileHelper) { - super($logger, $options, $staticConfig, $processService, $prompter, $userSettingsService, $analyticsSettingsService); } - public trackException(exception: any, message: string): Promise { - const data: IExceptionsTrackingInformation = { - type: TrackingTypes.Exception, - exception, - message - }; + public setShouldDispose(shouldDispose: boolean): void { + this.shouldDisposeInstance = shouldDispose; + } - return this.sendInfoForTracking(data, this.$staticConfig.ERROR_REPORT_SETTING_NAME); + public async checkConsent(): Promise { + if (await this.$analyticsSettingsService.canDoRequest()) { + const initialTrackFeatureUsageStatus = await this.getStatus(this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME); + let trackFeatureUsage = initialTrackFeatureUsageStatus === AnalyticsStatus.enabled; + + if (await this.isNotConfirmed(this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME) && isInteractive()) { + this.$logger.out("Do you want to help us improve " + + this.$analyticsSettingsService.getClientName() + + " by automatically sending anonymous usage statistics? We will not use this information to identify or contact you." + + " You can read our official Privacy Policy at"); + + const message = this.$analyticsSettingsService.getPrivacyPolicyLink(); + trackFeatureUsage = await this.$prompter.confirm(message, () => true); + await this.setStatus(this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME, trackFeatureUsage); + + await this.trackAcceptFeatureUsage({ acceptTrackFeatureUsage: trackFeatureUsage }); + } + + const isErrorReportingUnset = await this.isNotConfirmed(this.$staticConfig.ERROR_REPORT_SETTING_NAME); + const isUsageReportingConfirmed = !await this.isNotConfirmed(this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME); + if (isErrorReportingUnset && isUsageReportingConfirmed) { + await this.setStatus(this.$staticConfig.ERROR_REPORT_SETTING_NAME, trackFeatureUsage); + } + } + } + + public async setStatus(settingName: string, enabled: boolean): Promise { + this.analyticsStatuses[settingName] = enabled ? AnalyticsStatus.enabled : AnalyticsStatus.disabled; + await this.$userSettingsService.saveSetting(settingName, enabled.toString()); + } + + public async isEnabled(settingName: string): Promise { + const analyticsStatus = await this.getStatus(settingName); + return analyticsStatus === AnalyticsStatus.enabled; + } + + public getStatusMessage(settingName: string, jsonFormat: boolean, readableSettingName: string): Promise { + if (jsonFormat) { + return this.getJsonStatusMessage(settingName); + } + + return this.getHumanReadableStatusMessage(settingName, readableSettingName); } public async trackAcceptFeatureUsage(settings: { acceptTrackFeatureUsage: boolean }): Promise { @@ -192,6 +230,72 @@ export class AnalyticsService extends AnalyticsServiceBase { } }); } + + @cache() + private async initAnalyticsStatuses(): Promise { + if (await this.$analyticsSettingsService.canDoRequest()) { + this.$logger.trace("Initializing analytics statuses."); + const settingsNames = [this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME, this.$staticConfig.ERROR_REPORT_SETTING_NAME]; + + for (const settingName of settingsNames) { + await this.getStatus(settingName); + } + + this.$logger.trace("Analytics statuses: ", this.analyticsStatuses); + } + } + + private async getStatus(settingName: string): Promise { + if (!_.has(this.analyticsStatuses, settingName)) { + const settingValue = await this.$userSettingsService.getSettingValue(settingName); + + if (settingValue) { + const isEnabled = toBoolean(settingValue); + if (isEnabled) { + this.analyticsStatuses[settingName] = AnalyticsStatus.enabled; + } else { + this.analyticsStatuses[settingName] = AnalyticsStatus.disabled; + } + } else { + this.analyticsStatuses[settingName] = AnalyticsStatus.notConfirmed; + } + } + + return this.analyticsStatuses[settingName]; + } + + private async isNotConfirmed(settingName: string): Promise { + const analyticsStatus = await this.getStatus(settingName); + return analyticsStatus === AnalyticsStatus.notConfirmed; + } + + private async getHumanReadableStatusMessage(settingName: string, readableSettingName: string): Promise { + let status: string = null; + + if (await this.isNotConfirmed(settingName)) { + status = "disabled until confirmed"; + } else { + status = await this.getStatus(settingName); + } + + return `${readableSettingName} is ${status}.`; + } + + private async getJsonStatusMessage(settingName: string): Promise { + const status = await this.getStatus(settingName); + const enabled = status === AnalyticsStatus.notConfirmed ? null : status === AnalyticsStatus.enabled; + return JSON.stringify({ enabled }); + } + + public trackException(exception: any, message: string): Promise { + const data: IExceptionsTrackingInformation = { + type: TrackingTypes.Exception, + exception, + message + }; + + return this.sendInfoForTracking(data, this.$staticConfig.ERROR_REPORT_SETTING_NAME); + } } $injector.register("analyticsService", AnalyticsService); From 0b89ee529bd1196298c75951afc0c4704c39147c Mon Sep 17 00:00:00 2001 From: Rosen Vladimirov Date: Mon, 19 Nov 2018 23:30:39 +0200 Subject: [PATCH 4/5] feat(analytics): write analytics data to passed file Write analytics data to a file passed as `--analyticsLogFile`. The file will contain timestamp when the action is executed and what is the data passed to analytics. Also add some additional logging to the file, so when something fails, it will be easier for diagnostics. In case `--analyticsLogFile` is not passed, information will not be tracked. --- lib/declarations.d.ts | 1 + lib/options.ts | 1 + .../analytics/analytics-broker-process.ts | 18 +++++++- .../analytics/analytics-logging-service.ts | 41 +++++++++++++++++++ lib/services/analytics/analytics-service.ts | 5 ++- lib/services/analytics/analytics.d.ts | 20 +++++++++ .../analytics/google-analytics-provider.ts | 38 ++++++++++++++--- 7 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 lib/services/analytics/analytics-logging-service.ts diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 32dfc873e1..eafdaab489 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -557,6 +557,7 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai background: string; hmr: boolean; link: boolean; + analyticsLogFile: string; } interface IEnvOptions { diff --git a/lib/options.ts b/lib/options.ts index 59e6a78798..821430db09 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -114,6 +114,7 @@ export class Options { var: { type: OptionType.Object }, default: { type: OptionType.Boolean }, count: { type: OptionType.Number }, + analyticsLogFile: { type: OptionType.String }, hooks: { type: OptionType.Boolean, default: true }, link: { type: OptionType.Boolean, default: false }, aab: { type: OptionType.Boolean } diff --git a/lib/services/analytics/analytics-broker-process.ts b/lib/services/analytics/analytics-broker-process.ts index 02b806aa75..6efd6d925f 100644 --- a/lib/services/analytics/analytics-broker-process.ts +++ b/lib/services/analytics/analytics-broker-process.ts @@ -2,16 +2,22 @@ // The instances here are not shared with the ones in main CLI process. import * as fs from "fs"; import { AnalyticsBroker } from "./analytics-broker"; +import { AnalyticsLoggingService } from "./analytics-logging-service"; const pathToBootstrap = process.argv[2]; if (!pathToBootstrap || !fs.existsSync(pathToBootstrap)) { throw new Error("Invalid path to bootstrap."); } +const logFile = process.argv[3]; // After requiring the bootstrap we can use $injector require(pathToBootstrap); -const analyticsBroker = $injector.resolve(AnalyticsBroker, { pathToBootstrap }); +const analyticsLoggingService = $injector.resolve(AnalyticsLoggingService, { logFile }); +analyticsLoggingService.logData({ message: "Initializing AnalyticsBroker." }); + +const analyticsBroker = $injector.resolve(AnalyticsBroker, { pathToBootstrap, analyticsLoggingService }); + let trackingQueue: Promise = Promise.resolve(); let sentFinishMsg = false; @@ -23,12 +29,15 @@ const sendDataForTracking = async (data: ITrackingInformation) => { }; const finishTracking = async (data?: ITrackingInformation) => { + analyticsLoggingService.logData({ message: `analytics-broker-process finish tracking started, sentFinishMsg: ${sentFinishMsg}, receivedFinishMsg: ${receivedFinishMsg}` }); + if (!sentFinishMsg) { sentFinishMsg = true; data = data || { type: TrackingTypes.Finish }; const action = async () => { - await sendDataForTracking(data); + await trackingQueue; + analyticsLoggingService.logData({ message: `analytics-broker-process tracking finished` }); process.disconnect(); }; @@ -46,6 +55,8 @@ const finishTracking = async (data?: ITrackingInformation) => { }; process.on("message", async (data: ITrackingInformation) => { + analyticsLoggingService.logData({ message: `analytics-broker-process received message of type: ${data.type}` }); + if (data.type === TrackingTypes.Finish) { receivedFinishMsg = true; await finishTracking(data); @@ -56,7 +67,10 @@ process.on("message", async (data: ITrackingInformation) => { }); process.on("disconnect", async () => { + analyticsLoggingService.logData({ message: "analytics-broker-process received process.disconnect event" }); await finishTracking(); }); +analyticsLoggingService.logData({ message: `analytics-broker-process will send ${AnalyticsMessages.BrokerReadyToReceive} message` }); + process.send(AnalyticsMessages.BrokerReadyToReceive); diff --git a/lib/services/analytics/analytics-logging-service.ts b/lib/services/analytics/analytics-logging-service.ts new file mode 100644 index 0000000000..feb1423d61 --- /dev/null +++ b/lib/services/analytics/analytics-logging-service.ts @@ -0,0 +1,41 @@ +import { EOL } from "os"; + +export class AnalyticsLoggingService implements IAnalyticsLoggingService { + constructor(private $fs: IFileSystem, + private logFile: string) { } + + public logData(analyticsLoggingMessage: IAnalyticsLoggingMessage): void { + if (this.logFile && analyticsLoggingMessage && analyticsLoggingMessage.message) { + analyticsLoggingMessage.type = analyticsLoggingMessage.type || AnalyticsLoggingMessageType.Info; + const formattedDate = this.getFormattedDate(); + this.$fs.appendFile(this.logFile, `[${formattedDate}] [${analyticsLoggingMessage.type}] ${analyticsLoggingMessage.message}${EOL}`); + } + } + + private getFormattedDate(): string { + const currentDate = new Date(); + const year = currentDate.getFullYear(); + const month = this.getFormattedDateComponent((currentDate.getMonth() + 1)); + const day = this.getFormattedDateComponent(currentDate.getDate()); + const hour = this.getFormattedDateComponent(currentDate.getHours()); + const minutes = this.getFormattedDateComponent(currentDate.getMinutes()); + const seconds = this.getFormattedDateComponent(currentDate.getSeconds()); + const milliseconds = this.getFormattedMilliseconds(currentDate); + + return `${[year, month, day].join('-')} ${[hour, minutes, seconds].join(":")}.${milliseconds}`; + } + + private getFormattedDateComponent(component: number): string { + const stringComponent = component.toString(); + return stringComponent.length === 1 ? `0${stringComponent}` : stringComponent; + } + + private getFormattedMilliseconds(date: Date): string { + let milliseconds = date.getMilliseconds().toString(); + while (milliseconds.length < 3) { + milliseconds = `0${milliseconds}`; + } + + return milliseconds; + } +} diff --git a/lib/services/analytics/analytics-service.ts b/lib/services/analytics/analytics-service.ts index 97d508cf66..3ea2036cdb 100644 --- a/lib/services/analytics/analytics-service.ts +++ b/lib/services/analytics/analytics-service.ts @@ -147,10 +147,11 @@ export class AnalyticsService implements IAnalyticsService, IDisposable { @cache() private getAnalyticsBroker(): Promise { return new Promise((resolve, reject) => { - const broker = this.$childProcess.spawn("node", + const broker = this.$childProcess.spawn(process.execPath, [ path.join(__dirname, "analytics-broker-process.js"), - this.$staticConfig.PATH_TO_BOOTSTRAP + this.$staticConfig.PATH_TO_BOOTSTRAP, + this.$options.analyticsLogFile // TODO: Check if passing path with space or quotes will work ], { stdio: ["ignore", "ignore", "ignore", "ipc"], diff --git a/lib/services/analytics/analytics.d.ts b/lib/services/analytics/analytics.d.ts index 97f5992a63..0fdaf5e16d 100644 --- a/lib/services/analytics/analytics.d.ts +++ b/lib/services/analytics/analytics.d.ts @@ -58,3 +58,23 @@ interface IGoogleAnalyticsProvider { */ trackHit(data: IGoogleAnalyticsData): Promise; } + +/** + * Describes message that needs to be logged in the analytics logging file. + */ +interface IAnalyticsLoggingMessage { + message: string; + type?: AnalyticsLoggingMessageType +} + +/** + * Describes methods to get local logs from analytics tracking. + */ +interface IAnalyticsLoggingService { + /** + * Logs specified message to the file specified with `--analyticsLogFile`. + * @param {IAnalyticsLoggingMessage} analyticsLoggingMessage The message that has to be written to the logs file. + * @returns {void} + */ + logData(analyticsLoggingMessage: IAnalyticsLoggingMessage): void; +} diff --git a/lib/services/analytics/google-analytics-provider.ts b/lib/services/analytics/google-analytics-provider.ts index 20806e9bc5..53cb7bcdd3 100644 --- a/lib/services/analytics/google-analytics-provider.ts +++ b/lib/services/analytics/google-analytics-provider.ts @@ -1,6 +1,7 @@ import * as uuid from "uuid"; import * as ua from "universal-analytics"; import { AnalyticsClients } from "../../common/constants"; +import { cache } from "../../common/decorators"; export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { private currentPage: string; @@ -10,7 +11,8 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { private $analyticsSettingsService: IAnalyticsSettingsService, private $logger: ILogger, private $proxyService: IProxyService, - private $config: IConfiguration) { + private $config: IConfiguration, + private analyticsLoggingService: IAnalyticsLoggingService) { } public async trackHit(trackInfo: IGoogleAnalyticsData): Promise { @@ -19,13 +21,14 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { try { await this.track(this.$config.GA_TRACKING_ID, trackInfo, sessionId); } catch (e) { + this.analyticsLoggingService.logData({ type: AnalyticsLoggingMessageType.Error, message: `Unable to track information ${JSON.stringify(trackInfo)}. Error is: ${e}` }); this.$logger.trace("Analytics exception: ", e); } } - private async track(gaTrackingId: string, trackInfo: IGoogleAnalyticsData, sessionId: string): Promise { - const proxySettings = await this.$proxyService.getCache(); - const proxy = proxySettings && proxySettings.proxy; + @cache() + private getVisitor(gaTrackingId: string, proxy: string): ua.Visitor { + this.analyticsLoggingService.logData({ message: `Initializing Google Analytics visitor for id: ${gaTrackingId} with clientId: ${this.clientId}.` }); const visitor = ua({ tid: gaTrackingId, cid: this.clientId, @@ -34,9 +37,20 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { }, requestOptions: { proxy - } + }, + https: true }); + this.analyticsLoggingService.logData({ message: `Successfully initialized Google Analytics visitor for id: ${gaTrackingId} with clientId: ${this.clientId}.` }); + return visitor; + } + + private async track(gaTrackingId: string, trackInfo: IGoogleAnalyticsData, sessionId: string): Promise { + const proxySettings = await this.$proxyService.getCache(); + const proxy = proxySettings && proxySettings.proxy; + + const visitor = this.getVisitor(gaTrackingId, proxy); + await this.setCustomDimensions(visitor, trackInfo.customDimensions, sessionId); switch (trackInfo.googleAnalyticsDataType) { @@ -68,6 +82,7 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { customDimensions = _.merge(defaultValues, customDimensions); _.each(customDimensions, (value, key) => { + this.analyticsLoggingService.logData({ message: `Setting custom dimension ${key} to value ${value}` }); visitor.set(key, value); }); } @@ -76,10 +91,17 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { return new Promise((resolve, reject) => { visitor.event(trackInfo.category, trackInfo.action, trackInfo.label, trackInfo.value, { p: this.currentPage }, (err: Error) => { if (err) { + this.analyticsLoggingService.logData({ + message: `Unable to track event with category: '${trackInfo.category}', action: '${trackInfo.action}', label: '${trackInfo.label}', ` + + `value: '${trackInfo.value}' attached page: ${this.currentPage}. Error is: ${err}.`, + type: AnalyticsLoggingMessageType.Error + }); + reject(err); return; } + this.analyticsLoggingService.logData({ message: `Tracked event with category: '${trackInfo.category}', action: '${trackInfo.action}', label: '${trackInfo.label}', value: '${trackInfo.value}' attached page: ${this.currentPage}.` }); resolve(); }); }); @@ -96,10 +118,16 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { visitor.pageview(pageViewData, (err) => { if (err) { + this.analyticsLoggingService.logData({ + message: `Unable to track pageview with path '${trackInfo.path}' and title: '${trackInfo.title}' Error is: ${err}.`, + type: AnalyticsLoggingMessageType.Error + }); + reject(err); return; } + this.analyticsLoggingService.logData({ message: `Tracked pageview with path '${trackInfo.path}' and title: '${trackInfo.title}'.` }); resolve(); }); }); From 9ee3ee80dd20c055e9a2616ae32d6ce67f69f895 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Tue, 20 Nov 2018 10:39:25 +0200 Subject: [PATCH 5/5] chore: set correct TODOs in the code --- lib/common/commands/analytics.ts | 4 ++-- lib/common/commands/proxy/proxy-base.ts | 2 +- lib/services/doctor-service.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/common/commands/analytics.ts b/lib/common/commands/analytics.ts index 7f01a8110b..52e9d10871 100644 --- a/lib/common/commands/analytics.ts +++ b/lib/common/commands/analytics.ts @@ -31,11 +31,11 @@ class AnalyticsCommand implements ICommand { switch (arg.toLowerCase()) { case "enable": await this.$analyticsService.setStatus(this.settingName, true); - // await this.$analyticsService.track(this.settingName, "enabled"); + // TODO(Analytics): await this.$analyticsService.track(this.settingName, "enabled"); this.$logger.info(`${this.humanReadableSettingName} is now enabled.`); break; case "disable": - // await this.$analyticsService.track(this.settingName, "disabled"); + // TODO(Analytics): await this.$analyticsService.track(this.settingName, "disabled"); await this.$analyticsService.setStatus(this.settingName, false); this.$logger.info(`${this.humanReadableSettingName} is now disabled.`); break; diff --git a/lib/common/commands/proxy/proxy-base.ts b/lib/common/commands/proxy/proxy-base.ts index e6be6cdd21..0dc9d879a2 100644 --- a/lib/common/commands/proxy/proxy-base.ts +++ b/lib/common/commands/proxy/proxy-base.ts @@ -12,7 +12,7 @@ export abstract class ProxyCommandBase implements ICommand { protected async tryTrackUsage() { try { - // TODO: Check why we have set the `disableAnalytics` to true and we track the command as separate one + // TODO(Analytics): Check why we have set the `disableAnalytics` to true and we track the command as separate one // instead of tracking it through the commandsService. this.$logger.trace(this.commandName); // await this.$analyticsService.trackFeature(this.commandName); diff --git a/lib/services/doctor-service.ts b/lib/services/doctor-service.ts index 79531eb513..4e93ff0124 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -31,7 +31,7 @@ class DoctorService implements IDoctorService { } if (!configOptions || configOptions.trackResult) { - // TODO: Consider sending this information to Google Analytics + // TODO(Analytics): Consider sending this information to Google Analytics // await this.$analyticsService.track("DoctorEnvironmentSetup", hasWarnings ? "incorrect" : "correct"); }