From 1f3fa087414e958be07c251a5bcce9e8c6996316 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Thu, 24 Aug 2017 14:30:11 +0300 Subject: [PATCH 1/4] Improve analytics We have a requirement to track information in several different providers. However, this may slow down CLI's operations as each track (at least for some of the providers) may last around 100-300ms. In order to track in multiple analytics provider without slowing down CLI commands, move the tracking to a new process. The process is detached from CLI, so when CLI finishes its work it will exit, while the child process will remain alive until it sends all required information. The new process is called `analytics-broker-process`, it is detached from CLI, but it there's IPC channel between them, so CLI can send information to be tracked. Just before CLI dies, it will send "finish" message to the `broker` process and the IPC channel will be closed. In CLI implement a new `analytics-service`, so when the methods `track`, `trackFeature` and `trackException` are called, instead of tracking directly in CLI's process (i.e. the old implementation), to send information to the broker process. The only exception is tracking of the answer if user allows us to send anonymous statistics. As this action requires user interaction, it is executed in CLI's process, a new eqatec analytics monitor is started and the answer is sent to it. After that the analytics is stopped. In case the answer is "yes" and there is more information that we want to track in the current process, it will be send to broker and tracked in all providers. In case the answer is "no", no other data will be tracked. The broker process is started from analytics process once per process, i.e. on each CLI process (which have something to track) there's a corresponding `analytics-broker-process`. Once it is initialized, it sends BrokerReadyToReceive message to CLI and CLI knows that it can start sending tracking information. Initialization phase is creating a new instance of AnalyticsBroker and attaching handlers for `message` and `disconnect` events. The AnalyitcsBroker itself has a single purpose - sends tracking data to all available prociders. Currently the only provider is `eqatecAnalyticsProvider`. Once CLI sends "finish" message (or `analytics-broker-process` receives `disconnect` event), the process sends information to all providers that there's nothing more to be tracked. When all providers finish their work, they should disocnnect themselves from the broker process and it will exit gracefully. `EqatecAnalyticsProvider` is used to track features and exceptions in Progress Telerik Analytics. Our requirement is to track features in two projects (same data should be send to both of them) and exceptions in another project. Each of them should be in a separate child process, so failure in one of them will not reflect the others. So the provider starts all required processes. When broker sends information to the provider, it sends it to all required child processes, for example if a feature should be tracked, the provider will send information for it to the two processes used for feature tracking. Each eqatec child process is a new Node.js process runnign `eqatec-analytics-process`. Again - this is a detached process. Once it is initialized, it sends "EqatecAnalyticsReadyToReceive" message to the `EqatecAnalyitcsProvider` and it knows it can send information to the new child process. The initialization phase is creation of new instance of `EqatectAnalyticsService`, attaching to "message" and "disconnect" events. Once `EqatecAnalyitcsProvider` receives `EqatecAnalyticsReadyToReceive` message, it sends initialization data for the eqatec monitor. It includes the analytics project API key, user id, current sessions count, etc. The information is persisted in a file on user's machine, so this file should be updated from a single process. As we might have 3 eqatec analytics processes, the provider is the only place where the file should be read and updated. When `eqatec-analytics-process` receives initialization data, it creates a new instance of the `EqatectAnalyticsService` class with it. However a monitor is not started yet. It will be started when there's something to be tracked, i.e. once `eqatec-analytics-process` receives feature or exception to be tracked. When `eqatec-analytics-process` receives "finish" message, it awaits all pending requests and after that disconnets itself from the parent. In case this operation fails, the process just exits, which will allow the parent to exit gracefully. Remove the public `startEqatecMonitor` method from API - it has not been used. --- .vscode/launch.json | 15 +- PublicAPI.md | 15 -- lib/bootstrap.ts | 3 +- lib/common | 2 +- lib/config.ts | 2 +- lib/services/analytics-service.ts | 34 ---- lib/services/analytics-settings-service.ts | 11 +- .../analytics/analytics-broker-process.ts | 56 ++++++ lib/services/analytics/analytics-broker.ts | 39 +++++ lib/services/analytics/analytics-constants.ts | 14 ++ lib/services/analytics/analytics-service.ts | 125 +++++++++++++ lib/services/analytics/analytics.d.ts | 66 +++++++ .../analytics/eqatec-analytics-process.ts | 89 ++++++++++ .../analytics/eqatec-analytics-provider.ts | 165 ++++++++++++++++++ lib/services/analytics/eqatec-analytics.ts | 52 ++++++ test/nativescript-cli-lib.ts | 1 - 16 files changed, 625 insertions(+), 64 deletions(-) delete mode 100644 lib/services/analytics-service.ts create mode 100644 lib/services/analytics/analytics-broker-process.ts create mode 100644 lib/services/analytics/analytics-broker.ts create mode 100644 lib/services/analytics/analytics-constants.ts create mode 100644 lib/services/analytics/analytics-service.ts create mode 100644 lib/services/analytics/analytics.d.ts create mode 100644 lib/services/analytics/eqatec-analytics-process.ts create mode 100644 lib/services/analytics/eqatec-analytics-provider.ts create mode 100644 lib/services/analytics/eqatec-analytics.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index acf4559e49..88a6442f47 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -67,8 +67,19 @@ { "type": "node", "request": "attach", - "name": "Attach to Process", - "port": 5858, + "name": "Attach to Broker Process", + // In case you want to debug Analytics Broker process, add `--debug-brk=9897` (or --inspect-brk=9897) when spawning analytics-broker-process. + "port": 9897, + "sourceMaps": true + }, + + { + "type": "node", + "request": "attach", + "name": "Attach to Eqatec Process", + // In case you want to debug Eqatec Analytics process, add `--debug-brk=9855` (or --inspect-brk=9855) when spawning eqatec-analytics-process. + // NOTE: Ensure you set it only for one of the analytics processes. + "port": 9855, "sourceMaps": true } diff --git a/PublicAPI.md b/PublicAPI.md index a84043d074..206287aee4 100644 --- a/PublicAPI.md +++ b/PublicAPI.md @@ -394,21 +394,6 @@ tns.npm.view(["nativescript"], {}).then(result => { }); ``` -## analyticsService -Provides a way to configure analytics. - -### startEqatecMonitor -* Definition: -```TypeScript -/** - * Starts analytics monitor with provided key. - * @param {string} projectApiKey API key with which to start analytics monitor. - * @returns {Promise}. - */ -startEqatecMonitor(projectApiKey: string): Promise; -``` - - ## debugService Provides methods for debugging applications on devices. The service is also event emitter, that raises the following events: * `connectionError` event - this event is raised when the debug operation cannot start on iOS device. The causes can be: diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 96c6f24eda..8319bba9f9 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -30,7 +30,8 @@ $injector.require("androidDebugService", "./services/android-debug-service"); $injector.require("userSettingsService", "./services/user-settings-service"); $injector.require("analyticsSettingsService", "./services/analytics-settings-service"); -$injector.requirePublic("analyticsService", "./services/analytics-service"); +$injector.requirePublic("analyticsService", "./services/analytics/analytics-service"); +$injector.require("eqatecAnalyticsProvider", "./services/analytics/eqatec-analytics-provider"); $injector.require("emulatorSettingsService", "./services/emulator-settings-service"); diff --git a/lib/common b/lib/common index 5b4c6da25c..76cb359549 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 5b4c6da25ca015f52756431ddc63b5a7fbd8a995 +Subproject commit 76cb359549adc6ebc83ebb34a9a644da2f3352b7 diff --git a/lib/config.ts b/lib/config.ts index 247c721ede..4b6b21c942 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -73,7 +73,7 @@ export class StaticConfig extends StaticConfigBase implements IStaticConfig { } public get PATH_TO_BOOTSTRAP(): string { - return path.join(__dirname, "bootstrap"); + return path.join(__dirname, "bootstrap.js"); } public async getAdbFilePath(): Promise { diff --git a/lib/services/analytics-service.ts b/lib/services/analytics-service.ts deleted file mode 100644 index ba98c7f662..0000000000 --- a/lib/services/analytics-service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { AnalyticsServiceBase } from "../common/services/analytics-service-base"; -import { exported } from "../common/decorators"; - -export class AnalyticsService extends AnalyticsServiceBase implements IAnalyticsService { - private static ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY = "9912cff308334c6d9ad9c33f76a983e3"; - - constructor(protected $logger: ILogger, - protected $options: IOptions, - $staticConfig: Config.IStaticConfig, - $prompter: IPrompter, - $userSettingsService: UserSettings.IUserSettingsService, - $analyticsSettingsService: IAnalyticsSettingsService, - $progressIndicator: IProgressIndicator, - $osInfo: IOsInfo) { - super($logger, $options, $staticConfig, $prompter, $userSettingsService, $analyticsSettingsService, $progressIndicator, $osInfo); - } - - @exported("analyticsService") - public async startEqatecMonitor(projectApiKey: string): Promise { - if (await this.isEnabled(this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME) || await this.isEnabled(this.$staticConfig.ERROR_REPORT_SETTING_NAME)) { - await this.restartEqatecMonitor(projectApiKey); - } - } - - protected async checkConsentCore(trackFeatureUsage: boolean): Promise { - await this.restartEqatecMonitor(AnalyticsService.ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY); - await super.checkConsentCore(trackFeatureUsage); - - // Stop the monitor, so correct API_KEY will be used when features are tracked. - this.tryStopEqatecMonitor(); - } -} - -$injector.register("analyticsService", AnalyticsService); diff --git a/lib/services/analytics-settings-service.ts b/lib/services/analytics-settings-service.ts index 2147bbd273..d3ccd21d4c 100644 --- a/lib/services/analytics-settings-service.ts +++ b/lib/services/analytics-settings-service.ts @@ -1,7 +1,6 @@ import { createGUID } from "../common/helpers"; class AnalyticsSettingsService implements IAnalyticsSettingsService { - private static SESSIONS_STARTED_OBSOLETE_KEY = "SESSIONS_STARTED"; private static SESSIONS_STARTED_KEY_PREFIX = "SESSIONS_STARTED_"; constructor(private $userSettingsService: UserSettings.IUserSettingsService, @@ -33,14 +32,8 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService { } public async getUserSessionsCount(projectName: string): Promise { - const oldSessionCount = await this.$userSettingsService.getSettingValue(AnalyticsSettingsService.SESSIONS_STARTED_OBSOLETE_KEY); - - if (oldSessionCount) { - // remove the old property for sessions count - await this.$userSettingsService.removeSetting(AnalyticsSettingsService.SESSIONS_STARTED_OBSOLETE_KEY); - } - - return await this.$userSettingsService.getSettingValue(this.getSessionsProjectKey(projectName)) || oldSessionCount || 0; + const sessionsCountForProject = await this.$userSettingsService.getSettingValue(this.getSessionsProjectKey(projectName)); + return sessionsCountForProject || 0; } public async setUserSessionsCount(count: number, projectName: string): Promise { diff --git a/lib/services/analytics/analytics-broker-process.ts b/lib/services/analytics/analytics-broker-process.ts new file mode 100644 index 0000000000..cfcf9ed406 --- /dev/null +++ b/lib/services/analytics/analytics-broker-process.ts @@ -0,0 +1,56 @@ +import * as fs from "fs"; +import { AnalyticsBroker } from "./analytics-broker"; + +const pathToBootstrap = process.argv[2]; +if (!pathToBootstrap || !fs.existsSync(pathToBootstrap)) { + throw new Error("Invalid path to bootstrap."); +} + +// After requiring the bootstrap we can use $injector +require(pathToBootstrap); + +const analyticsBroker = $injector.resolve(AnalyticsBroker, { pathToBootstrap }); +let trackingQueue: Promise = Promise.resolve(); + +let sentFinishMsg = false; +let receivedFinishMsg = false; + +const sendDataForTracking = async (data: ITrackingInformation) => { + trackingQueue = trackingQueue.then(() => analyticsBroker.sendDataForTracking(data)); + await trackingQueue; +}; + +const finishTracking = async (data?: ITrackingInformation) => { + if (!sentFinishMsg) { + sentFinishMsg = true; + + data = data || { type: TrackingTypes.Finish }; + + if (receivedFinishMsg) { + await sendDataForTracking(data); + } else { + // In case we've got here without receiving "finish" message from parent (receivedFinishMsg is false) + // there might be various reasons, but most probably the parent is dead. + // However, there's no guarantee that we've received all messages. So wait some time before sending finish message to children. + setTimeout(async () => { + await sendDataForTracking(data); + }, 1000); + } + } +}; + +process.on("message", async (data: ITrackingInformation) => { + if (data.type === TrackingTypes.Finish) { + receivedFinishMsg = true; + await finishTracking(data); + return; + } + + await sendDataForTracking(data); +}); + +process.on("disconnect", async () => { + await finishTracking(); +}); + +process.send(AnalyticsMessages.BrokerReadyToReceive); diff --git a/lib/services/analytics/analytics-broker.ts b/lib/services/analytics/analytics-broker.ts new file mode 100644 index 0000000000..e450449c34 --- /dev/null +++ b/lib/services/analytics/analytics-broker.ts @@ -0,0 +1,39 @@ +import { cache } from "../../common/decorators"; + +export class AnalyticsBroker implements IAnalyticsBroker { + + @cache() + private get $eqatecAnalyticsProvider(): IAnalyticsProvider { + return this.$injector.resolve("eqatecAnalyticsProvider", { pathToBootstrap: this.pathToBootstrap }); + } + + constructor(private pathToBootstrap: string, + private $injector: IInjector) { } + + private get analyticsProviders(): IAnalyticsProvider[] { + return [ + this.$eqatecAnalyticsProvider + ]; + } + + public async sendDataForTracking(trackInfo: ITrackingInformation): Promise { + for (const provider of this.analyticsProviders) { + switch (trackInfo.type) { + case TrackingTypes.Exception: + await provider.trackException(trackInfo); + break; + case TrackingTypes.Feature: + await provider.trackFeature(trackInfo); + break; + case TrackingTypes.Finish: + await provider.finishTracking(); + break; + default: + throw new Error(`Invalid tracking type: ${trackInfo.type}`); + } + + } + + } + +} diff --git a/lib/services/analytics/analytics-constants.ts b/lib/services/analytics/analytics-constants.ts new file mode 100644 index 0000000000..5aff79435c --- /dev/null +++ b/lib/services/analytics/analytics-constants.ts @@ -0,0 +1,14 @@ +/** + * Defines messages used in communication between CLI's process and analytics subprocesses. + */ +const enum AnalyticsMessages { + /** + * Analytics Broker is initialized and is ready to receive information for tracking. + */ + BrokerReadyToReceive = "BrokerReadyToReceive", + + /** + * Eqatec Analytics process is initialized and is ready to receive information for tracking. + */ + EqatecAnalyticsReadyToReceive = "EqatecAnalyticsReadyToReceive" +} diff --git a/lib/services/analytics/analytics-service.ts b/lib/services/analytics/analytics-service.ts new file mode 100644 index 0000000000..6afe8c4fe5 --- /dev/null +++ b/lib/services/analytics/analytics-service.ts @@ -0,0 +1,125 @@ +import { AnalyticsServiceBase } from "../../common/services/analytics-service-base"; +import { ChildProcess } from "child_process"; +import * as path from "path"; +import { cache } from "../../common/decorators"; + +export class AnalyticsService extends AnalyticsServiceBase implements IAnalyticsService { + private static ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY = "9912cff308334c6d9ad9c33f76a983e3"; + private static ANALYTICS_BROKER_START_TIMEOUT = 30 * 1000; + + constructor(protected $logger: ILogger, + protected $options: IOptions, + $staticConfig: Config.IStaticConfig, + $prompter: IPrompter, + $userSettingsService: UserSettings.IUserSettingsService, + $analyticsSettingsService: IAnalyticsSettingsService, + $progressIndicator: IProgressIndicator, + $osInfo: IOsInfo, + private $childProcess: IChildProcess, + private $processService: IProcessService) { + super($logger, $options, $staticConfig, $prompter, $userSettingsService, $analyticsSettingsService, $progressIndicator, $osInfo); + } + + public track(featureName: string, featureValue: string): Promise { + return this.sendDataForTracking(featureName, featureValue); + } + + public trackException(exception: any, message: string): Promise { + return this.sendExceptionForTracking(exception, message); + } + + protected async checkConsentCore(trackFeatureUsage: boolean): Promise { + await this.restartEqatecMonitor(AnalyticsService.ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY); + await super.checkConsentCore(trackFeatureUsage); + + // Stop the monitor, so correct API_KEY will be used when features are tracked. + this.tryStopEqatecMonitor(); + } + + @cache() + private getAnalyticsBroker(): Promise { + return new Promise((resolve, reject) => { + const broker = this.$childProcess.spawn("node", + [ + path.join(__dirname, "analytics-broker-process.js"), + this.$staticConfig.PATH_TO_BOOTSTRAP + ], + { + stdio: ["ignore", "ignore", "ignore", "ipc"], + detached: true + } + ); + + broker.unref(); + + let isSettled = false; + + const timeoutId = setTimeout(() => { + if (!isSettled) { + reject(new Error("Unable to start Analytics Broker process.")); + } + }, AnalyticsService.ANALYTICS_BROKER_START_TIMEOUT); + + broker.on("error", (err: Error) => { + clearTimeout(timeoutId); + + if (!isSettled) { + isSettled = true; + reject(err); + } + }); + + broker.on("message", (data: any) => { + if (data === AnalyticsMessages.BrokerReadyToReceive) { + clearTimeout(timeoutId); + + if (!isSettled) { + isSettled = true; + resolve(broker); + } + } + }); + + this.$processService.attachToProcessExitSignals(this, () => { + broker.send({ + type: TrackingTypes.Finish + }); + }); + }); + } + + private async sendDataForTracking(featureName: string, featureValue: string): Promise { + await this.initAnalyticsStatuses(); + + if (this.analyticsStatuses[this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME] === AnalyticsStatus.enabled) { + return this.sendMessageToBroker( + { + type: TrackingTypes.Feature, + featureName: featureName, + featureValue: featureValue + } + ); + } + } + + private async sendExceptionForTracking(exception: Error, message: string): Promise { + await this.initAnalyticsStatuses(); + + if (this.analyticsStatuses[this.$staticConfig.ERROR_REPORT_SETTING_NAME] === AnalyticsStatus.enabled) { + return this.sendMessageToBroker( + { + type: TrackingTypes.Exception, + exception, + message + } + ); + } + } + + private async sendMessageToBroker(message: any): Promise { + const broker = await this.getAnalyticsBroker(); + return new Promise((resolve, reject) => broker.send(message, resolve)); + } +} + +$injector.register("analyticsService", AnalyticsService); diff --git a/lib/services/analytics/analytics.d.ts b/lib/services/analytics/analytics.d.ts new file mode 100644 index 0000000000..18ba857b0f --- /dev/null +++ b/lib/services/analytics/analytics.d.ts @@ -0,0 +1,66 @@ +/** + * Describes information used for tracking feature. + */ +interface IFeatureTrackingInformation extends ITrackingInformation { + /** + * The name of the feature that should be tracked. + */ + featureName: string; + + /** + * Value of the feature that should be tracked. + */ + featureValue: string; +} + +/** + * Describes information for exception that should be tracked. + */ +interface IExceptionsTrackingInformation extends ITrackingInformation { + /** + * The exception that should be tracked. + */ + exception: Error; + + /** + * The message of the error that should be tracked. + */ + message: string; +} + +/** + * Describes the broker used to pass information to all analytics providers. + */ +interface IAnalyticsBroker { + /** + * Sends the specified tracking information to all providers. + * @param {ITrackingInformation} trackInfo The information that should be passed to all providers. + * @returns {Promise} + */ + 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} + */ + trackException(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} + */ + trackFeature(trackInfo: IFeatureTrackingInformation): Promise; + + /** + * Waits for execution of all pending requests and finishes tracking operation + * @returns {Promise} + */ + finishTracking(): Promise; +} diff --git a/lib/services/analytics/eqatec-analytics-process.ts b/lib/services/analytics/eqatec-analytics-process.ts new file mode 100644 index 0000000000..3a00ed3f53 --- /dev/null +++ b/lib/services/analytics/eqatec-analytics-process.ts @@ -0,0 +1,89 @@ +import * as fs from "fs"; +import { EqatectAnalyticsService } from "./eqatec-analytics"; + +const pathToBootstrap = process.argv[2]; +if (!pathToBootstrap || !fs.existsSync(pathToBootstrap)) { + throw new Error("Invalid path to bootstrap."); +} + +// After requiring the bootstrap we can use $injector +require(pathToBootstrap); + +let eqatecAnalyticsService: EqatectAnalyticsService = null; + +const assertEqatecAnalyticsIsInitialized = () => { + if (!eqatecAnalyticsService) { + throw new Error("Analytics service is not initialized yet. Unable to send data."); + } +}; + +let trackingQueue = Promise.resolve(); + +let receivedFinishMsg = false; +let hasStoppedEqatecProcess = false; + +const stopAnalyticsProcess = async () => { + try { + trackingQueue = trackingQueue.then(() => eqatecAnalyticsService.tryStopEqatecMonitor()); + await trackingQueue; + + eqatecAnalyticsService = null; + process.disconnect(); + } catch (err) { + process.exit(ErrorCodes.FAILED_EQATEC_ANALYTICS_PROCESS); + } +}; + +const finishTracking = async () => { + if (!hasStoppedEqatecProcess) { + hasStoppedEqatecProcess = true; + + if (receivedFinishMsg) { + await stopAnalyticsProcess(); + } else { + // In case we've got here without receiving "finish" message from parent (receivedFinishMsg is false) + // there might be various reasons, but most probably the parent is dead. + // However, there's no guarantee that we've received all messages. So wait some time before sending finish message to children. + setTimeout(async () => { + await stopAnalyticsProcess(); + }, 1000); + } + } +}; + +process.on("message", async (data: ITrackingInformation) => { + switch (data.type) { + case TrackingTypes.Initialization: + const analyticsSettings = data; + eqatecAnalyticsService = $injector.resolve(EqatectAnalyticsService, { analyticsSettings }); + + break; + case TrackingTypes.Feature: + assertEqatecAnalyticsIsInitialized(); + const trackFeatureInfo = data; + trackingQueue = trackingQueue.then(() => eqatecAnalyticsService.trackInformation(trackFeatureInfo)); + await trackingQueue; + + break; + case TrackingTypes.Exception: + assertEqatecAnalyticsIsInitialized(); + const exceptionInfo = data; + trackingQueue = trackingQueue.then(() => eqatecAnalyticsService.trackError(exceptionInfo)); + await trackingQueue; + + break; + + case TrackingTypes.Finish: + receivedFinishMsg = true; + await finishTracking(); + break; + default: + break; + } +}); + +process.on("disconnect", async () => { + await finishTracking(); +}); + +process.send(AnalyticsMessages.EqatecAnalyticsReadyToReceive); diff --git a/lib/services/analytics/eqatec-analytics-provider.ts b/lib/services/analytics/eqatec-analytics-provider.ts new file mode 100644 index 0000000000..d959df5d9e --- /dev/null +++ b/lib/services/analytics/eqatec-analytics-provider.ts @@ -0,0 +1,165 @@ +import { ChildProcess } from "child_process"; +import * as path from "path"; + +import { createGUID } from "../../common/helpers"; +import { cache } from "../../common/decorators"; + +export class EqatecAnalyticsProvider implements IAnalyticsProvider { + private static NEW_PROJECT_ANALYTICS_API_KEY = "b40f24fcb4f94bccaf64e4dc6337422e"; + private _eqatecExceptionChildProcess: ChildProcess = null; + private _eqatecFeatureChildProcesses: ChildProcess[] = []; + + @cache() + private get pathToEqatecEntryPoint(): string { + return path.join(__dirname, "eqatec-analytics-process.js"); + } + + private get eqatecFeatureChildProcesses(): ChildProcess[] { + return this._eqatecFeatureChildProcesses; + } + + private get _eqatecFeatureProjectAPIKeys(): string[] { + return [ + this.$staticConfig.ANALYTICS_API_KEY, + EqatecAnalyticsProvider.NEW_PROJECT_ANALYTICS_API_KEY + ]; + } + + private get _eqatecExceptionProjectAPIKey(): string { + return this.$staticConfig.ANALYTICS_EXCEPTIONS_API_KEY; + } + + constructor(private pathToBootstrap: string, + private $staticConfig: Config.IStaticConfig, + private $userSettingsService: UserSettings.IUserSettingsService, + private $analyticsSettingsService: IAnalyticsSettingsService, + private $childProcess: IChildProcess) { + } + + public async trackException(trackInfo: IExceptionsTrackingInformation): Promise { + const eqatecExceptionChildProcess = await this.getEqatecProcessForExceptions(); + eqatecExceptionChildProcess.send(trackInfo); + } + + public async trackFeature(trackInfo: IFeatureTrackingInformation): Promise { + await this.initializeEqatecProcessesForFeatureTracking(); + + for (const eqatecChildProcess of this.eqatecFeatureChildProcesses) { + eqatecChildProcess.send(trackInfo); + } + } + + public async finishTracking(): Promise { + const trackInfo: ITrackingInformation = { + type: TrackingTypes.Finish + }; + + for (const eqatecChildProcess of this.eqatecFeatureChildProcesses) { + eqatecChildProcess.send(trackInfo); + } + + const eqatecExceptionChildProcess = await this.getEqatecProcessForExceptions({ skipInitialization: true }); + if (eqatecExceptionChildProcess) { + eqatecExceptionChildProcess.send(trackInfo); + } + } + + @cache() + private async initializeEqatecProcessesForFeatureTracking(): Promise { + const analyticsInstallationId = await this.getEqatecInstallationId(); + const userId = await this.$analyticsSettingsService.getUserId(); + + for (const analyticsAPIKey of this._eqatecFeatureProjectAPIKeys) { + let userSessionCount = await this.$analyticsSettingsService.getUserSessionsCount(analyticsAPIKey); + await this.$analyticsSettingsService.setUserSessionsCount(++userSessionCount, analyticsAPIKey); + const trackingInformation = { + analyticsInstallationId, + analyticsAPIKey, + type: TrackingTypes.Initialization, + userId, + userSessionCount + }; + + await this.initializeEqatecFeatureChildProcess(trackingInformation); + } + } + + @cache() + private async getEqatecInstallationId(): Promise { + let analyticsInstallationId = await this.$userSettingsService.getSettingValue(this.$staticConfig.ANALYTICS_INSTALLATION_ID_SETTING_NAME); + if (!analyticsInstallationId) { + analyticsInstallationId = createGUID(false); + await this.$userSettingsService.saveSetting(this.$staticConfig.ANALYTICS_INSTALLATION_ID_SETTING_NAME, analyticsInstallationId); + } + + return analyticsInstallationId; + } + + private initializeEqatecChildProcess(eqatecInitData: IEqatecInitializeData): Promise { + return new Promise((resolve, reject) => { + const eqatecChildProcess: ChildProcess = this.$childProcess.spawn("node", [ + this.pathToEqatecEntryPoint, + this.pathToBootstrap + ], + { + stdio: ["ignore", "ignore", "ignore", "ipc"], + detached: true + } + ); + + eqatecChildProcess.unref(); + + eqatecChildProcess.on("message", (data) => { + if (data === AnalyticsMessages.EqatecAnalyticsReadyToReceive) { + eqatecChildProcess.send(eqatecInitData, () => { + resolve(eqatecChildProcess); + }); + } + }); + + eqatecChildProcess.on("error", (err: Error) => { + reject(err); + }); + + return eqatecChildProcess; + }); + } + + private async initializeEqatecFeatureChildProcess(eqatecInitData: IEqatecInitializeData): Promise { + const eqatecFeatureChildProcesses = await this.initializeEqatecChildProcess(eqatecInitData); + this.eqatecFeatureChildProcesses.push(eqatecFeatureChildProcesses); + } + + @cache() + private async initializeEqatecChildProcessForExceptions(): Promise { + const analyticsInstallationId = await this.getEqatecInstallationId(); + const userId = await this.$analyticsSettingsService.getUserId(); + let userSessionCount = await this.$analyticsSettingsService.getUserSessionsCount(this._eqatecExceptionProjectAPIKey); + + await this.$analyticsSettingsService.setUserSessionsCount(++userSessionCount, this._eqatecExceptionProjectAPIKey); + + const trackingInformation = { + analyticsInstallationId, + analyticsAPIKey: this._eqatecExceptionProjectAPIKey, + type: TrackingTypes.Initialization, + userId, + userSessionCount + }; + + const eqatecExceptionChildProcess = await this.initializeEqatecChildProcess(trackingInformation); + + return eqatecExceptionChildProcess; + } + + private async getEqatecProcessForExceptions(processSettings?: { skipInitialization: boolean }): Promise { + const callerRequiresChildProcess = !processSettings || !processSettings.skipInitialization; + + if (!this._eqatecExceptionChildProcess && callerRequiresChildProcess) { + this._eqatecExceptionChildProcess = await this.initializeEqatecChildProcessForExceptions(); + } + + return this._eqatecExceptionChildProcess; + } +} + +$injector.register("eqatecAnalyticsProvider", EqatecAnalyticsProvider); diff --git a/lib/services/analytics/eqatec-analytics.ts b/lib/services/analytics/eqatec-analytics.ts new file mode 100644 index 0000000000..3ba2ea7670 --- /dev/null +++ b/lib/services/analytics/eqatec-analytics.ts @@ -0,0 +1,52 @@ +import { AnalyticsServiceBase } from "../../common/services/analytics-service-base"; + +export class EqatectAnalyticsService extends AnalyticsServiceBase { + + constructor(private analyticsSettings: IEqatecInitializeData, + protected $logger: ILogger, + protected $staticConfig: Config.IStaticConfig, + $options: ICommonOptions, + $prompter: IPrompter, + $userSettingsService: UserSettings.IUserSettingsService, + $analyticsSettingsService: IAnalyticsSettingsService, + $progressIndicator: IProgressIndicator, + $osInfo: IOsInfo) { + + super($logger, $options, $staticConfig, $prompter, $userSettingsService, $analyticsSettingsService, $progressIndicator, $osInfo); + } + + public async trackInformation(data: IFeatureTrackingInformation): Promise { + try { + await this.startEqatecMonitor(this.analyticsSettings); + + if (this._eqatecMonitor) { + this._eqatecMonitor.trackFeature(`${data.featureName}.${data.featureValue}`); + await this.waitForSending(); + } + } catch (e) { + this.$logger.trace(`Analytics exception: ${e}`); + } + } + + public async trackError(data: IExceptionsTrackingInformation): Promise { + try { + await this.startEqatecMonitor(this.analyticsSettings); + + if (this._eqatecMonitor) { + this._eqatecMonitor.trackException(data.exception, data.message); + await this.waitForSending(); + } + } catch (e) { + this.$logger.trace(`Analytics exception: ${e}`); + } + } + + public tryStopEqatecMonitor(code?: string | number): void { + if (this._eqatecMonitor) { + // remove the listener for exit event and explicitly call stop of monitor + process.removeListener("exit", this.tryStopEqatecMonitor); + this._eqatecMonitor.stop(); + this._eqatecMonitor = null; + } + } +} diff --git a/test/nativescript-cli-lib.ts b/test/nativescript-cli-lib.ts index 2168f38da0..10854e48f0 100644 --- a/test/nativescript-cli-lib.ts +++ b/test/nativescript-cli-lib.ts @@ -21,7 +21,6 @@ describe("nativescript-cli-lib", () => { npm: ["install", "uninstall", "view", "search"], extensibilityService: ["loadExtensions", "loadExtension", "getInstalledExtensions", "installExtension", "uninstallExtension"], liveSyncService: ["liveSync", "stopLiveSync", "enableDebugging", "disableDebugging", "attachDebugger"], - analyticsService: ["startEqatecMonitor"], debugService: ["debug"] }; From 4b80f8172b33654c9aeeb97ade01fdedb271d800 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Tue, 29 Aug 2017 15:03:24 +0300 Subject: [PATCH 2/4] Craete all eqatec analytics monitors in the broker process Eqatec Analytics monitors can be created in the broker process directly instead of starting multiple child processes. So delete some unneeded files and move tracking of `AcceptUsageReporting` out of CLI process. --- lib/common | 2 +- lib/services/analytics/analytics-broker.ts | 7 +- lib/services/analytics/analytics-service.ts | 20 +- lib/services/analytics/analytics.d.ts | 21 +- .../analytics/eqatec-analytics-process.ts | 89 --------- .../analytics/eqatec-analytics-provider.ts | 187 ++++-------------- lib/services/analytics/eqatec-analytics.ts | 52 ----- 7 files changed, 73 insertions(+), 305 deletions(-) delete mode 100644 lib/services/analytics/eqatec-analytics-process.ts delete mode 100644 lib/services/analytics/eqatec-analytics.ts diff --git a/lib/common b/lib/common index 76cb359549..3ee10c5600 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 76cb359549adc6ebc83ebb34a9a644da2f3352b7 +Subproject commit 3ee10c56007f260371548ae7e438ddce7023989c diff --git a/lib/services/analytics/analytics-broker.ts b/lib/services/analytics/analytics-broker.ts index e450449c34..24e373bdce 100644 --- a/lib/services/analytics/analytics-broker.ts +++ b/lib/services/analytics/analytics-broker.ts @@ -20,10 +20,13 @@ export class AnalyticsBroker implements IAnalyticsBroker { for (const provider of this.analyticsProviders) { switch (trackInfo.type) { case TrackingTypes.Exception: - await provider.trackException(trackInfo); + await provider.trackError(trackInfo); break; case TrackingTypes.Feature: - await provider.trackFeature(trackInfo); + await provider.trackInformation(trackInfo); + break; + case TrackingTypes.AcceptTrackFeatureUsage: + await provider.acceptFeatureUsageTracking(trackInfo); break; case TrackingTypes.Finish: await provider.finishTracking(); diff --git a/lib/services/analytics/analytics-service.ts b/lib/services/analytics/analytics-service.ts index 6afe8c4fe5..f0e7ab9e23 100644 --- a/lib/services/analytics/analytics-service.ts +++ b/lib/services/analytics/analytics-service.ts @@ -4,7 +4,6 @@ import * as path from "path"; import { cache } from "../../common/decorators"; export class AnalyticsService extends AnalyticsServiceBase implements IAnalyticsService { - private static ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY = "9912cff308334c6d9ad9c33f76a983e3"; private static ANALYTICS_BROKER_START_TIMEOUT = 30 * 1000; constructor(protected $logger: ILogger, @@ -13,11 +12,10 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics $prompter: IPrompter, $userSettingsService: UserSettings.IUserSettingsService, $analyticsSettingsService: IAnalyticsSettingsService, - $progressIndicator: IProgressIndicator, $osInfo: IOsInfo, private $childProcess: IChildProcess, private $processService: IProcessService) { - super($logger, $options, $staticConfig, $prompter, $userSettingsService, $analyticsSettingsService, $progressIndicator, $osInfo); + super($logger, $options, $staticConfig, $prompter, $userSettingsService, $analyticsSettingsService, $osInfo); } public track(featureName: string, featureValue: string): Promise { @@ -28,12 +26,12 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics return this.sendExceptionForTracking(exception, message); } - protected async checkConsentCore(trackFeatureUsage: boolean): Promise { - await this.restartEqatecMonitor(AnalyticsService.ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY); - await super.checkConsentCore(trackFeatureUsage); + public async trackAcceptFeatureUsage(settings: { acceptTrackFeatureUsage: boolean }): Promise { - // Stop the monitor, so correct API_KEY will be used when features are tracked. - this.tryStopEqatecMonitor(); + this.sendMessageToBroker( { + type: TrackingTypes.AcceptTrackFeatureUsage, + acceptTrackFeatureUsage: settings.acceptTrackFeatureUsage + }); } @cache() @@ -93,7 +91,7 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics if (this.analyticsStatuses[this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME] === AnalyticsStatus.enabled) { return this.sendMessageToBroker( - { + { type: TrackingTypes.Feature, featureName: featureName, featureValue: featureValue @@ -107,7 +105,7 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics if (this.analyticsStatuses[this.$staticConfig.ERROR_REPORT_SETTING_NAME] === AnalyticsStatus.enabled) { return this.sendMessageToBroker( - { + { type: TrackingTypes.Exception, exception, message @@ -116,7 +114,7 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics } } - private async sendMessageToBroker(message: any): Promise { + private async sendMessageToBroker(message: ITrackingInformation): Promise { const broker = await this.getAnalyticsBroker(); return new Promise((resolve, reject) => broker.send(message, resolve)); } diff --git a/lib/services/analytics/analytics.d.ts b/lib/services/analytics/analytics.d.ts index 18ba857b0f..5a59a4229a 100644 --- a/lib/services/analytics/analytics.d.ts +++ b/lib/services/analytics/analytics.d.ts @@ -1,3 +1,13 @@ +/** + * Describes if the user allows to be tracked. + */ +interface IAcceptUsageReportingInformation extends ITrackingInformation { + /** + * The answer of the question if user allows us to track them. + */ + acceptTrackFeatureUsage: boolean; +} + /** * Describes information used for tracking feature. */ @@ -49,14 +59,21 @@ interface IAnalyticsProvider { * @param {IExceptionsTrackingInformation} trackInfo The information for exception that should be tracked. * @returns {Promise} */ - trackException(trackInfo: IExceptionsTrackingInformation): 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} */ - trackFeature(trackInfo: IFeatureTrackingInformation): 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 diff --git a/lib/services/analytics/eqatec-analytics-process.ts b/lib/services/analytics/eqatec-analytics-process.ts deleted file mode 100644 index 3a00ed3f53..0000000000 --- a/lib/services/analytics/eqatec-analytics-process.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as fs from "fs"; -import { EqatectAnalyticsService } from "./eqatec-analytics"; - -const pathToBootstrap = process.argv[2]; -if (!pathToBootstrap || !fs.existsSync(pathToBootstrap)) { - throw new Error("Invalid path to bootstrap."); -} - -// After requiring the bootstrap we can use $injector -require(pathToBootstrap); - -let eqatecAnalyticsService: EqatectAnalyticsService = null; - -const assertEqatecAnalyticsIsInitialized = () => { - if (!eqatecAnalyticsService) { - throw new Error("Analytics service is not initialized yet. Unable to send data."); - } -}; - -let trackingQueue = Promise.resolve(); - -let receivedFinishMsg = false; -let hasStoppedEqatecProcess = false; - -const stopAnalyticsProcess = async () => { - try { - trackingQueue = trackingQueue.then(() => eqatecAnalyticsService.tryStopEqatecMonitor()); - await trackingQueue; - - eqatecAnalyticsService = null; - process.disconnect(); - } catch (err) { - process.exit(ErrorCodes.FAILED_EQATEC_ANALYTICS_PROCESS); - } -}; - -const finishTracking = async () => { - if (!hasStoppedEqatecProcess) { - hasStoppedEqatecProcess = true; - - if (receivedFinishMsg) { - await stopAnalyticsProcess(); - } else { - // In case we've got here without receiving "finish" message from parent (receivedFinishMsg is false) - // there might be various reasons, but most probably the parent is dead. - // However, there's no guarantee that we've received all messages. So wait some time before sending finish message to children. - setTimeout(async () => { - await stopAnalyticsProcess(); - }, 1000); - } - } -}; - -process.on("message", async (data: ITrackingInformation) => { - switch (data.type) { - case TrackingTypes.Initialization: - const analyticsSettings = data; - eqatecAnalyticsService = $injector.resolve(EqatectAnalyticsService, { analyticsSettings }); - - break; - case TrackingTypes.Feature: - assertEqatecAnalyticsIsInitialized(); - const trackFeatureInfo = data; - trackingQueue = trackingQueue.then(() => eqatecAnalyticsService.trackInformation(trackFeatureInfo)); - await trackingQueue; - - break; - case TrackingTypes.Exception: - assertEqatecAnalyticsIsInitialized(); - const exceptionInfo = data; - trackingQueue = trackingQueue.then(() => eqatecAnalyticsService.trackError(exceptionInfo)); - await trackingQueue; - - break; - - case TrackingTypes.Finish: - receivedFinishMsg = true; - await finishTracking(); - break; - default: - break; - } -}); - -process.on("disconnect", async () => { - await finishTracking(); -}); - -process.send(AnalyticsMessages.EqatecAnalyticsReadyToReceive); diff --git a/lib/services/analytics/eqatec-analytics-provider.ts b/lib/services/analytics/eqatec-analytics-provider.ts index d959df5d9e..e9539372a9 100644 --- a/lib/services/analytics/eqatec-analytics-provider.ts +++ b/lib/services/analytics/eqatec-analytics-provider.ts @@ -1,165 +1,56 @@ -import { ChildProcess } from "child_process"; -import * as path from "path"; +import { AnalyticsServiceBase } from "../../common/services/analytics-service-base"; -import { createGUID } from "../../common/helpers"; -import { cache } from "../../common/decorators"; - -export class EqatecAnalyticsProvider implements IAnalyticsProvider { +export class EqatecAnalyticsProvider extends AnalyticsServiceBase implements IAnalyticsProvider { + private static ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY = "9912cff308334c6d9ad9c33f76a983e3"; private static NEW_PROJECT_ANALYTICS_API_KEY = "b40f24fcb4f94bccaf64e4dc6337422e"; - private _eqatecExceptionChildProcess: ChildProcess = null; - private _eqatecFeatureChildProcesses: ChildProcess[] = []; - - @cache() - private get pathToEqatecEntryPoint(): string { - return path.join(__dirname, "eqatec-analytics-process.js"); - } - - private get eqatecFeatureChildProcesses(): ChildProcess[] { - return this._eqatecFeatureChildProcesses; - } - - private get _eqatecFeatureProjectAPIKeys(): string[] { - return [ - this.$staticConfig.ANALYTICS_API_KEY, - EqatecAnalyticsProvider.NEW_PROJECT_ANALYTICS_API_KEY - ]; - } - - private get _eqatecExceptionProjectAPIKey(): string { - return this.$staticConfig.ANALYTICS_EXCEPTIONS_API_KEY; - } - - constructor(private pathToBootstrap: string, - private $staticConfig: Config.IStaticConfig, - private $userSettingsService: UserSettings.IUserSettingsService, - private $analyticsSettingsService: IAnalyticsSettingsService, - private $childProcess: IChildProcess) { - } - public async trackException(trackInfo: IExceptionsTrackingInformation): Promise { - const eqatecExceptionChildProcess = await this.getEqatecProcessForExceptions(); - eqatecExceptionChildProcess.send(trackInfo); - } - - public async trackFeature(trackInfo: IFeatureTrackingInformation): Promise { - await this.initializeEqatecProcessesForFeatureTracking(); - - for (const eqatecChildProcess of this.eqatecFeatureChildProcesses) { - eqatecChildProcess.send(trackInfo); + 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, + $staticConfig: Config.IStaticConfig, + $prompter: IPrompter, + $userSettingsService: UserSettings.IUserSettingsService, + $analyticsSettingsService: IAnalyticsSettingsService, + $osInfo: IOsInfo) { + super($logger, $options, $staticConfig, $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 finishTracking(): Promise { - const trackInfo: ITrackingInformation = { - type: TrackingTypes.Finish - }; - - for (const eqatecChildProcess of this.eqatecFeatureChildProcesses) { - eqatecChildProcess.send(trackInfo); - } - - const eqatecExceptionChildProcess = await this.getEqatecProcessForExceptions({ skipInitialization: true }); - if (eqatecExceptionChildProcess) { - eqatecExceptionChildProcess.send(trackInfo); + public async trackError(data: IExceptionsTrackingInformation): Promise { + try { + await this.trackException(data.exception, data.message); + } catch (e) { + this.$logger.trace(`Analytics exception: ${e}`); } } - @cache() - private async initializeEqatecProcessesForFeatureTracking(): Promise { - const analyticsInstallationId = await this.getEqatecInstallationId(); - const userId = await this.$analyticsSettingsService.getUserId(); - - for (const analyticsAPIKey of this._eqatecFeatureProjectAPIKeys) { - let userSessionCount = await this.$analyticsSettingsService.getUserSessionsCount(analyticsAPIKey); - await this.$analyticsSettingsService.setUserSessionsCount(++userSessionCount, analyticsAPIKey); - const trackingInformation = { - analyticsInstallationId, - analyticsAPIKey, - type: TrackingTypes.Initialization, - userId, - userSessionCount - }; - - await this.initializeEqatecFeatureChildProcess(trackingInformation); + public async acceptFeatureUsageTracking(data: IAcceptUsageReportingInformation): Promise { + try { + await this.trackAcceptFeatureUsage({ acceptTrackFeatureUsage: data.acceptTrackFeatureUsage }); + } catch (e) { + this.$logger.trace(`Analytics exception: ${e}`); } } - @cache() - private async getEqatecInstallationId(): Promise { - let analyticsInstallationId = await this.$userSettingsService.getSettingValue(this.$staticConfig.ANALYTICS_INSTALLATION_ID_SETTING_NAME); - if (!analyticsInstallationId) { - analyticsInstallationId = createGUID(false); - await this.$userSettingsService.saveSetting(this.$staticConfig.ANALYTICS_INSTALLATION_ID_SETTING_NAME, analyticsInstallationId); - } - - return analyticsInstallationId; - } - - private initializeEqatecChildProcess(eqatecInitData: IEqatecInitializeData): Promise { - return new Promise((resolve, reject) => { - const eqatecChildProcess: ChildProcess = this.$childProcess.spawn("node", [ - this.pathToEqatecEntryPoint, - this.pathToBootstrap - ], - { - stdio: ["ignore", "ignore", "ignore", "ipc"], - detached: true - } - ); - - eqatecChildProcess.unref(); - - eqatecChildProcess.on("message", (data) => { - if (data === AnalyticsMessages.EqatecAnalyticsReadyToReceive) { - eqatecChildProcess.send(eqatecInitData, () => { - resolve(eqatecChildProcess); - }); - } - }); - - eqatecChildProcess.on("error", (err: Error) => { - reject(err); - }); - - return eqatecChildProcess; - }); - } - - private async initializeEqatecFeatureChildProcess(eqatecInitData: IEqatecInitializeData): Promise { - const eqatecFeatureChildProcesses = await this.initializeEqatecChildProcess(eqatecInitData); - this.eqatecFeatureChildProcesses.push(eqatecFeatureChildProcesses); - } - - @cache() - private async initializeEqatecChildProcessForExceptions(): Promise { - const analyticsInstallationId = await this.getEqatecInstallationId(); - const userId = await this.$analyticsSettingsService.getUserId(); - let userSessionCount = await this.$analyticsSettingsService.getUserSessionsCount(this._eqatecExceptionProjectAPIKey); - - await this.$analyticsSettingsService.setUserSessionsCount(++userSessionCount, this._eqatecExceptionProjectAPIKey); - - const trackingInformation = { - analyticsInstallationId, - analyticsAPIKey: this._eqatecExceptionProjectAPIKey, - type: TrackingTypes.Initialization, - userId, - userSessionCount - }; - - const eqatecExceptionChildProcess = await this.initializeEqatecChildProcess(trackingInformation); - - return eqatecExceptionChildProcess; + public async finishTracking(): Promise { + this.tryStopEqatecMonitors(); } - private async getEqatecProcessForExceptions(processSettings?: { skipInitialization: boolean }): Promise { - const callerRequiresChildProcess = !processSettings || !processSettings.skipInitialization; - - if (!this._eqatecExceptionChildProcess && callerRequiresChildProcess) { - this._eqatecExceptionChildProcess = await this.initializeEqatecChildProcessForExceptions(); - } - - return this._eqatecExceptionChildProcess; - } } $injector.register("eqatecAnalyticsProvider", EqatecAnalyticsProvider); diff --git a/lib/services/analytics/eqatec-analytics.ts b/lib/services/analytics/eqatec-analytics.ts deleted file mode 100644 index 3ba2ea7670..0000000000 --- a/lib/services/analytics/eqatec-analytics.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { AnalyticsServiceBase } from "../../common/services/analytics-service-base"; - -export class EqatectAnalyticsService extends AnalyticsServiceBase { - - constructor(private analyticsSettings: IEqatecInitializeData, - protected $logger: ILogger, - protected $staticConfig: Config.IStaticConfig, - $options: ICommonOptions, - $prompter: IPrompter, - $userSettingsService: UserSettings.IUserSettingsService, - $analyticsSettingsService: IAnalyticsSettingsService, - $progressIndicator: IProgressIndicator, - $osInfo: IOsInfo) { - - super($logger, $options, $staticConfig, $prompter, $userSettingsService, $analyticsSettingsService, $progressIndicator, $osInfo); - } - - public async trackInformation(data: IFeatureTrackingInformation): Promise { - try { - await this.startEqatecMonitor(this.analyticsSettings); - - if (this._eqatecMonitor) { - this._eqatecMonitor.trackFeature(`${data.featureName}.${data.featureValue}`); - await this.waitForSending(); - } - } catch (e) { - this.$logger.trace(`Analytics exception: ${e}`); - } - } - - public async trackError(data: IExceptionsTrackingInformation): Promise { - try { - await this.startEqatecMonitor(this.analyticsSettings); - - if (this._eqatecMonitor) { - this._eqatecMonitor.trackException(data.exception, data.message); - await this.waitForSending(); - } - } catch (e) { - this.$logger.trace(`Analytics exception: ${e}`); - } - } - - public tryStopEqatecMonitor(code?: string | number): void { - if (this._eqatecMonitor) { - // remove the listener for exit event and explicitly call stop of monitor - process.removeListener("exit", this.tryStopEqatecMonitor); - this._eqatecMonitor.stop(); - this._eqatecMonitor = null; - } - } -} From 3df2fb8c102e1e095d2955705443f6009eff35d5 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Tue, 5 Sep 2017 10:36:43 +0300 Subject: [PATCH 3/4] Add tracking to Google Analytics Add new methods to track information in Google Analytics(GA). As the data that we want to track is different than the one we track in Eqatec Analytics, call the new methods whenever we want to send some information to GA. Change the broker process to send information to Eqatec Analytics when we want to track feature or exception and to GA only when the new method for tracking there is called. In Google Analytics (GA) we track two types of data - pages (called pageviews) and events. Each command is tracked as a page - we only track the name of the command as a page: * `tns build android` will be tracked as visited page - `build android` * `tns run` will be tracked as visited page - `run` When we want to track some additional actions that happen during execution of each command, we send events to GA. Each event has the following data: * category - currently we do not have a specific category for each action, so it is hardcoded to "CLI". * action - this is the real action that will be tracked, for example `Build`, `LiveSync`, `Deploy`, etc. * label - contains specific inforamation for the current action, for example what is the platform, device type, etc. In many cases we send labels with a lot of information, but as the value must be string, we use `_` as a separator. For example: * Action `Build` may have the following label: `Android_Debug_Incremental`. This is `__`. * Action `LiveSync` may have the following label: `Android_Emulator_5.1`. This is `__`. Custom dimensions For each additional data may be send to GA via Custom Dimensions. More about them can be found [here](https://support.google.com/analytics/answer/2709828). We are using several custom dimensions, most of them have hit scope, i.e. their values are saved for the current hit (event or page), but may be changed for the next time. The only exclusion is `session id` - it has session scope, so whenever it is sent, the new value overwrites the values of the "session id" property in all hits of the current session. One interesting custom dimension is `Client` - it will be set to the value of `--analyticsClient` in case it is passed. In case not, we'll set it to "CLI" when terminal is interactive and "Unknown" when terminal is not interactive. Sessions - what is session for GA In Eqatec Analytics we call `stopMonitor` at the end of each command. So the session there is one command. In GA we've decided to keep the default way of measuring a session - a session is stopped when there are 30 inactive minutes, i.e. no data is sent from 30 minutes. This allows us to investigate the behavior and the flow of commands a user is executing in a single session (during the day for example). Client ID and User ID: Currently we do not send User ID to GA, we use Client ID instead as sending User IDs requires different logic and agreements. For us all users are anonymous. User-Agent When sending requests to GA, we need to set correct User-Agent, so GA will be able to understand the Operating System. For each OS the string is painfully specific... Current implementation works fine for Windows, Linux and macOS. Implementation In order to send data to GA, we are using a `universal-analytics` npm package. We cannot use Google's default libraries (`analytics.js`, `gtag.js`) as they are written for browser. So the only way to track data is to use the Measurement Protocol (directly send http requests). However, `universal-analytics` package provides a very good abstraction over the Measurement Protocol, so we are using it instead of implementing each call. More information about Measurement protocol: https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters --- lib/bootstrap.ts | 3 +- lib/common | 2 +- lib/constants.ts | 18 +++ lib/nativescript-cli-lib-bootstrap.ts | 1 + lib/services/analytics-settings-service.ts | 26 ++-- lib/services/analytics/analytics-broker.ts | 57 ++++---- lib/services/analytics/analytics-service.ts | 125 +++++++++++++----- lib/services/analytics/analytics.d.ts | 14 ++ .../google-analytics-custom-dimensions.ts | 8 ++ .../analytics/google-analytics-provider.ts | 120 +++++++++++++++++ lib/services/debug-service.ts | 11 +- lib/services/livesync/livesync-service.ts | 9 +- lib/services/platform-service.ts | 21 ++- lib/services/project-templates-service.ts | 6 + npm-shrinkwrap.json | 30 ++++- package.json | 6 +- test/plugins-service.ts | 5 +- test/project-service.ts | 5 +- test/project-templates-service.ts | 5 +- test/services/debug-service.ts | 4 + test/services/livesync-service.ts | 3 + 21 files changed, 394 insertions(+), 85 deletions(-) create mode 100644 lib/services/analytics/google-analytics-custom-dimensions.ts create mode 100644 lib/services/analytics/google-analytics-provider.ts diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 8319bba9f9..f4ad743cdd 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -30,8 +30,9 @@ $injector.require("androidDebugService", "./services/android-debug-service"); $injector.require("userSettingsService", "./services/user-settings-service"); $injector.require("analyticsSettingsService", "./services/analytics-settings-service"); -$injector.requirePublic("analyticsService", "./services/analytics/analytics-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/common b/lib/common index 3ee10c5600..7072607b66 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 3ee10c56007f260371548ae7e438ddce7023989c +Subproject commit 7072607b66f0394699b6bb36a5c66f366f3965fa diff --git a/lib/constants.ts b/lib/constants.ts index 346d475dab..6405df9471 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -103,3 +103,21 @@ export const enum NativePlatformStatus { requiresPrepare = "2", alreadyPrepared = "3" } + +export const enum DebugTools { + Chrome = "Chrome", + Inspector = "Inspector" +} + +export const enum TrackActionNames { + Build = "Build", + CreateProject = "Create project", + Debug = "Debug", + Deploy = "Deploy", + LiveSync = "LiveSync" +} + +export const enum BuildStates { + Clean = "Clean", + Incremental = "Incremental" +} diff --git a/lib/nativescript-cli-lib-bootstrap.ts b/lib/nativescript-cli-lib-bootstrap.ts index 95dcf39e2f..1e545239e3 100644 --- a/lib/nativescript-cli-lib-bootstrap.ts +++ b/lib/nativescript-cli-lib-bootstrap.ts @@ -13,3 +13,4 @@ $injector.requirePublicClass("localBuildService", "./services/local-build-servic // We need this because some services check if (!$options.justlaunch) to start the device log after some operation. // We don't want this behaviour when the CLI is required as library. $injector.resolve("options").justlaunch = true; +$injector.resolve("staticConfig").disableAnalytics = true; diff --git a/lib/services/analytics-settings-service.ts b/lib/services/analytics-settings-service.ts index d3ccd21d4c..6ca50db27e 100644 --- a/lib/services/analytics-settings-service.ts +++ b/lib/services/analytics-settings-service.ts @@ -11,16 +11,12 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService { return true; } - public async getUserId(): Promise { - let currentUserId = await this.$userSettingsService.getSettingValue("USER_ID"); - if (!currentUserId) { - currentUserId = createGUID(false); - - this.$logger.trace(`Setting new USER_ID: ${currentUserId}.`); - await this.$userSettingsService.saveSetting("USER_ID", currentUserId); - } + public getUserId(): Promise { + return this.getSettingValueOrDefault("USER_ID"); + } - return currentUserId; + public getClientId(): Promise { + return this.getSettingValueOrDefault(this.$staticConfig.ANALYTICS_INSTALLATION_ID_SETTING_NAME); } public getClientName(): string { @@ -43,5 +39,17 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService { private getSessionsProjectKey(projectName: string): string { return `${AnalyticsSettingsService.SESSIONS_STARTED_KEY_PREFIX}${projectName}`; } + + private async getSettingValueOrDefault(settingName: string): Promise { + let guid = await this.$userSettingsService.getSettingValue(settingName); + if (!guid) { + guid = createGUID(false); + + this.$logger.trace(`Setting new ${settingName}: ${guid}.`); + await this.$userSettingsService.saveSetting(settingName, guid); + } + + return guid; + } } $injector.register("analyticsSettingsService", AnalyticsSettingsService); diff --git a/lib/services/analytics/analytics-broker.ts b/lib/services/analytics/analytics-broker.ts index 24e373bdce..471840cd7b 100644 --- a/lib/services/analytics/analytics-broker.ts +++ b/lib/services/analytics/analytics-broker.ts @@ -3,38 +3,41 @@ import { cache } from "../../common/decorators"; export class AnalyticsBroker implements IAnalyticsBroker { @cache() - private get $eqatecAnalyticsProvider(): IAnalyticsProvider { - return this.$injector.resolve("eqatecAnalyticsProvider", { pathToBootstrap: this.pathToBootstrap }); + private async getEqatecAnalyticsProvider(): Promise { + return this.$injector.resolve("eqatecAnalyticsProvider"); } - constructor(private pathToBootstrap: string, - private $injector: IInjector) { } - - private get analyticsProviders(): IAnalyticsProvider[] { - return [ - this.$eqatecAnalyticsProvider - ]; + @cache() + private async getGoogleAnalyticsProvider(): Promise { + const clientId = await this.$analyticsSettingsService.getClientId(); + return this.$injector.resolve("googleAnalyticsProvider", { clientId }); } - public async sendDataForTracking(trackInfo: ITrackingInformation): Promise { - for (const provider of this.analyticsProviders) { - switch (trackInfo.type) { - case TrackingTypes.Exception: - await provider.trackError(trackInfo); - break; - case TrackingTypes.Feature: - await provider.trackInformation(trackInfo); - break; - case TrackingTypes.AcceptTrackFeatureUsage: - await provider.acceptFeatureUsageTracking(trackInfo); - break; - case TrackingTypes.Finish: - await provider.finishTracking(); - break; - default: - throw new Error(`Invalid tracking type: ${trackInfo.type}`); - } + constructor(private $analyticsSettingsService: IAnalyticsSettingsService, + private $injector: IInjector) { } + public async sendDataForTracking(trackInfo: ITrackingInformation): Promise { + 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}`); } } diff --git a/lib/services/analytics/analytics-service.ts b/lib/services/analytics/analytics-service.ts index f0e7ab9e23..d7d232a86c 100644 --- a/lib/services/analytics/analytics-service.ts +++ b/lib/services/analytics/analytics-service.ts @@ -2,6 +2,8 @@ import { AnalyticsServiceBase } from "../../common/services/analytics-service-ba import { ChildProcess } from "child_process"; import * as path from "path"; import { cache } from "../../common/decorators"; +import { isInteractive } from '../../common/helpers'; +import { DeviceTypes, AnalyticsClients } from "../../common/constants"; export class AnalyticsService extends AnalyticsServiceBase implements IAnalyticsService { private static ANALYTICS_BROKER_START_TIMEOUT = 30 * 1000; @@ -14,26 +16,100 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics $analyticsSettingsService: IAnalyticsSettingsService, $osInfo: IOsInfo, private $childProcess: IChildProcess, - private $processService: IProcessService) { + private $processService: IProcessService, + private $projectDataService: IProjectDataService, + private $mobileHelper: Mobile.IMobileHelper) { super($logger, $options, $staticConfig, $prompter, $userSettingsService, $analyticsSettingsService, $osInfo); } public track(featureName: string, featureValue: string): Promise { - return this.sendDataForTracking(featureName, featureValue); + const data: IFeatureTrackingInformation = { + type: TrackingTypes.Feature, + featureName: featureName, + featureValue: featureValue + }; + + return this.sendInfoForTracking(data, this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME); } public trackException(exception: any, message: string): Promise { - return this.sendExceptionForTracking(exception, message); + const data: IExceptionsTrackingInformation = { + type: TrackingTypes.Exception, + exception, + message + }; + + return this.sendInfoForTracking(data, this.$staticConfig.ERROR_REPORT_SETTING_NAME); } public async trackAcceptFeatureUsage(settings: { acceptTrackFeatureUsage: boolean }): Promise { - - this.sendMessageToBroker( { + this.sendMessageToBroker({ type: TrackingTypes.AcceptTrackFeatureUsage, acceptTrackFeatureUsage: settings.acceptTrackFeatureUsage }); } + public async trackInGoogleAnalytics(gaSettings: IGoogleAnalyticsData): Promise { + await this.initAnalyticsStatuses(); + + if (!this.$staticConfig.disableAnalytics && this.analyticsStatuses[this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME] === AnalyticsStatus.enabled) { + gaSettings.customDimensions = gaSettings.customDimensions || {}; + gaSettings.customDimensions[GoogleAnalyticsCustomDimensions.client] = this.$options.analyticsClient || (isInteractive() ? AnalyticsClients.Cli : AnalyticsClients.Unknown); + + const googleAnalyticsData: IGoogleAnalyticsTrackingInformation = _.merge({ type: TrackingTypes.GoogleAnalyticsData }, gaSettings, { category: AnalyticsClients.Cli }); + return this.sendMessageToBroker(googleAnalyticsData); + } + } + + public async trackEventActionInGoogleAnalytics(data: IEventActionData): Promise { + const device = data.device; + const platform = device ? device.deviceInfo.platform : data.platform; + const isForDevice = device ? !device.isEmulator : data.isForDevice; + + let label: string = ""; + label = this.addDataToLabel(label, platform); + + if (isForDevice !== null) { + // In case action is Build and platform is Android, we do not know if the deviceType is emulator or device. + // Just exclude the device_type in this case. + const deviceType = isForDevice ? DeviceTypes.Device : (this.$mobileHelper.isAndroidPlatform(platform) ? DeviceTypes.Emulator : DeviceTypes.Simulator); + label = this.addDataToLabel(label, deviceType); + } + + if (device) { + label = this.addDataToLabel(label, device.deviceInfo.version); + } + + if (data.additionalData) { + label = this.addDataToLabel(label, data.additionalData); + } + + const customDimensions: IStringDictionary = {}; + if (data.projectDir) { + const projectData = this.$projectDataService.getProjectData(data.projectDir); + customDimensions[GoogleAnalyticsCustomDimensions.projectType] = projectData.projectType; + } + + const googleAnalyticsEventData: IGoogleAnalyticsEventData = { + googleAnalyticsDataType: GoogleAnalyticsDataType.Event, + action: data.action, + label, + customDimensions + }; + + this.$logger.trace("Will send the following information to Google Analytics:", googleAnalyticsEventData); + + await this.trackInGoogleAnalytics(googleAnalyticsEventData); + } + + private addDataToLabel(label: string, newData: string): string { + if (newData && label) { + return `${label}_${newData}`; + } + + return label || newData || ""; + } + @cache() private getAnalyticsBroker(): Promise { return new Promise((resolve, reject) => { @@ -73,44 +149,25 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics if (!isSettled) { isSettled = true; + + this.$processService.attachToProcessExitSignals(this, () => { + broker.send({ + type: TrackingTypes.Finish + }); + }); + resolve(broker); } } }); - - this.$processService.attachToProcessExitSignals(this, () => { - broker.send({ - type: TrackingTypes.Finish - }); - }); }); } - private async sendDataForTracking(featureName: string, featureValue: string): Promise { - await this.initAnalyticsStatuses(); - - if (this.analyticsStatuses[this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME] === AnalyticsStatus.enabled) { - return this.sendMessageToBroker( - { - type: TrackingTypes.Feature, - featureName: featureName, - featureValue: featureValue - } - ); - } - } - - private async sendExceptionForTracking(exception: Error, message: string): Promise { + private async sendInfoForTracking(trackingInfo: ITrackingInformation, settingName: string): Promise { await this.initAnalyticsStatuses(); - if (this.analyticsStatuses[this.$staticConfig.ERROR_REPORT_SETTING_NAME] === AnalyticsStatus.enabled) { - return this.sendMessageToBroker( - { - type: TrackingTypes.Exception, - exception, - message - } - ); + if (!this.$staticConfig.disableAnalytics && this.analyticsStatuses[settingName] === AnalyticsStatus.enabled) { + return this.sendMessageToBroker(trackingInfo); } } diff --git a/lib/services/analytics/analytics.d.ts b/lib/services/analytics/analytics.d.ts index 5a59a4229a..ecb51c8b1f 100644 --- a/lib/services/analytics/analytics.d.ts +++ b/lib/services/analytics/analytics.d.ts @@ -81,3 +81,17 @@ interface IAnalyticsProvider { */ finishTracking(): Promise; } + +interface IGoogleAnalyticsTrackingInformation extends IGoogleAnalyticsData, ITrackingInformation { } + +/** + * Describes methods required to track in Google Analytics. + */ +interface IGoogleAnalyticsProvider { + /** + * Tracks hit types. + * @param {IGoogleAnalyticsData} data Data that has to be tracked. + * @returns {Promise} + */ + trackHit(data: IGoogleAnalyticsData): Promise; +} diff --git a/lib/services/analytics/google-analytics-custom-dimensions.ts b/lib/services/analytics/google-analytics-custom-dimensions.ts new file mode 100644 index 0000000000..9e7c1d7007 --- /dev/null +++ b/lib/services/analytics/google-analytics-custom-dimensions.ts @@ -0,0 +1,8 @@ +const enum GoogleAnalyticsCustomDimensions { + cliVersion = "cd1", + projectType = "cd2", + clientID = "cd3", + sessionID = "cd4", + client = "cd5", + nodeVersion = "cd6" +} diff --git a/lib/services/analytics/google-analytics-provider.ts b/lib/services/analytics/google-analytics-provider.ts new file mode 100644 index 0000000000..2e8055ca92 --- /dev/null +++ b/lib/services/analytics/google-analytics-provider.ts @@ -0,0 +1,120 @@ +import * as uuid from "uuid"; +import * as ua from "universal-analytics"; +import { AnalyticsClients } from "../../common/constants"; + +export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { + private static GA_TRACKING_ID = "UA-111455-44"; + private currentPage: string; + + constructor(private clientId: string, + private $staticConfig: IStaticConfig, + private $hostInfo: IHostInfo, + private $osInfo: IOsInfo) { + } + + public async trackHit(trackInfo: IGoogleAnalyticsData): Promise { + const visitor = ua({ + tid: GoogleAnalyticsProvider.GA_TRACKING_ID, + cid: this.clientId, + headers: { + ["User-Agent"]: this.getUserAgentString() + } + }); + + this.setCustomDimensions(visitor, trackInfo.customDimensions); + + switch (trackInfo.googleAnalyticsDataType) { + case GoogleAnalyticsDataType.Page: + await this.trackPageView(visitor, trackInfo); + break; + case GoogleAnalyticsDataType.Event: + await this.trackEvent(visitor, trackInfo); + break; + } + } + + private setCustomDimensions(visitor: ua.Visitor, customDimensions: IStringDictionary): void { + const defaultValues: IStringDictionary = { + [GoogleAnalyticsCustomDimensions.cliVersion]: this.$staticConfig.version, + [GoogleAnalyticsCustomDimensions.nodeVersion]: process.version, + [GoogleAnalyticsCustomDimensions.clientID]: this.clientId, + [GoogleAnalyticsCustomDimensions.projectType]: null, + [GoogleAnalyticsCustomDimensions.sessionID]: uuid.v4(), + [GoogleAnalyticsCustomDimensions.client]: AnalyticsClients.Unknown + }; + + customDimensions = _.merge(defaultValues, customDimensions); + + _.each(customDimensions, (value, key) => { + visitor.set(key, value); + }); + } + + private trackEvent(visitor: ua.Visitor, trackInfo: IGoogleAnalyticsEventData): Promise { + return new Promise((resolve, reject) => { + visitor.event(trackInfo.category, trackInfo.action, trackInfo.label, trackInfo.value, { p: this.currentPage }, (err: Error) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); + } + + private trackPageView(visitor: ua.Visitor, trackInfo: IGoogleAnalyticsPageviewData): Promise { + return new Promise((resolve, reject) => { + this.currentPage = trackInfo.path; + + const pageViewData: ua.PageviewParams = { + dp: trackInfo.path, + dt: trackInfo.title + }; + + visitor.pageview(pageViewData, (err) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); + } + + private getUserAgentString(): string { + let osString = ""; + const osRelease = this.$osInfo.release(); + + if (this.$hostInfo.isWindows) { + osString = `Windows NT ${osRelease}`; + } else if (this.$hostInfo.isDarwin) { + osString = `Macintosh`; + const macRelease = this.getMacOSReleaseVersion(osRelease); + if (macRelease) { + osString += `; Intel Mac OS X ${macRelease}`; + } + } else { + osString = `Linux x86`; + if (this.$osInfo.arch() === "x64") { + osString += "_64"; + } + } + + const userAgent = `tnsCli/${this.$staticConfig.version} (${osString}; ${this.$osInfo.arch()})`; + + return userAgent; + } + + private getMacOSReleaseVersion(osRelease: string): string { + // https://en.wikipedia.org/wiki/Darwin_(operating_system)#Release_history + // Each macOS version is labeled 10., where it looks like is taken from the major version returned by os.release() (16.x.x for example) and substituting 4 from it. + // So the version becomes "10.12" in this case. + // Could be improved by spawning `system_profiler SPSoftwareDataType` and getting the System Version line from the result. + const majorVersion = osRelease && _.first(osRelease.split(".")); + return majorVersion && `10.${+majorVersion - 4}`; + } +} + +$injector.register("googleAnalyticsProvider", GoogleAnalyticsProvider); diff --git a/lib/services/debug-service.ts b/lib/services/debug-service.ts index a8c314019b..ab6e730835 100644 --- a/lib/services/debug-service.ts +++ b/lib/services/debug-service.ts @@ -3,6 +3,7 @@ import { parse } from "url"; import { EventEmitter } from "events"; import { CONNECTION_ERROR_EVENT_NAME, DebugCommandErrors } from "../constants"; import { CONNECTED_STATUS } from "../common/constants"; +import { DebugTools, TrackActionNames } from "../constants"; export class DebugService extends EventEmitter implements IDebugService { private _platformDebugServices: IDictionary; @@ -10,7 +11,8 @@ export class DebugService extends EventEmitter implements IDebugService { private $errors: IErrors, private $injector: IInjector, private $hostInfo: IHostInfo, - private $mobileHelper: Mobile.IMobileHelper) { + private $mobileHelper: Mobile.IMobileHelper, + private $analyticsService: IAnalyticsService) { super(); this._platformDebugServices = {}; } @@ -26,6 +28,13 @@ export class DebugService extends EventEmitter implements IDebugService { this.$errors.failWithoutHelp(`The device with identifier ${debugData.deviceIdentifier} is unreachable. Make sure it is Trusted and try again.`); } + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: TrackActionNames.Debug, + device, + additionalData: this.$mobileHelper.isiOSPlatform(device.deviceInfo.platform) && (!options || !options.chrome) ? DebugTools.Inspector : DebugTools.Chrome, + projectDir: debugData.projectDir + }); + if (!(await device.applicationManager.isApplicationInstalled(debugData.applicationIdentifier))) { this.$errors.failWithoutHelp(`The application ${debugData.applicationIdentifier} is not installed on device with identifier ${debugData.deviceIdentifier}.`); } diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index e4bb9936c6..4d18546ea8 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -3,7 +3,7 @@ import * as choki from "chokidar"; import { EOL } from "os"; import { EventEmitter } from "events"; import { hook } from "../../common/helpers"; -import { APP_FOLDER_NAME, PACKAGE_JSON_FILE_NAME, LiveSyncTrackActionNames, USER_INTERACTION_NEEDED_EVENT_NAME, DEBUGGER_ATTACHED_EVENT_NAME, DEBUGGER_DETACHED_EVENT_NAME } from "../../constants"; +import { APP_FOLDER_NAME, PACKAGE_JSON_FILE_NAME, LiveSyncTrackActionNames, USER_INTERACTION_NEEDED_EVENT_NAME, DEBUGGER_ATTACHED_EVENT_NAME, DEBUGGER_DETACHED_EVENT_NAME, TrackActionNames } from "../../constants"; import { FileExtensions, DeviceTypes } from "../../common/constants"; const deviceDescriptorPrimaryKey = "identifier"; @@ -33,6 +33,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi private $debugService: IDebugService, private $errors: IErrors, private $debugDataService: IDebugDataService, + private $analyticsService: IAnalyticsService, private $injector: IInjector) { super(); } @@ -366,6 +367,12 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi action = LiveSyncTrackActionNames.LIVESYNC_OPERATION_BUILD; } + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: TrackActionNames.LiveSync, + device: options.device, + projectDir: options.projectData.projectDir + }); + await this.trackAction(action, platform, options); const shouldInstall = await this.$platformService.shouldInstall(options.device, options.projectData, options.deviceBuildInfoDescriptor.outputPath); diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 206fbcc2ec..268c2c2095 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -1,6 +1,7 @@ import * as path from "path"; import * as shell from "shelljs"; import * as constants from "../constants"; +import { Configurations } from "../common/constants"; import * as helpers from "../common/helpers"; import * as semver from "semver"; import { EventEmitter } from "events"; @@ -466,9 +467,18 @@ export class PlatformService extends EventEmitter implements IPlatformService { 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: "Build", platform, isForDevice }); + await this.trackActionForPlatform({ action, platform, isForDevice }); + + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action, + isForDevice, + platform, + projectDir: projectData.projectDir, + additionalData: `${buildConfig.release ? Configurations.Release : Configurations.Debug}_${buildConfig.clean ? constants.BuildStates.Clean : constants.BuildStates.Incremental}` + }); const platformData = this.$platformsData.getPlatformData(platform, projectData); const handler = (data: any) => { @@ -510,6 +520,13 @@ export class PlatformService extends EventEmitter implements IPlatformService { public async installApplication(device: Mobile.IDevice, buildConfig: IBuildConfig, projectData: IProjectData, packageFile?: string, outputFilePath?: string): Promise { this.$logger.out("Installing..."); + + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: constants.TrackActionNames.Deploy, + device, + projectDir: projectData.projectDir + }); + const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); if (!packageFile) { if (this.$devicesService.isiOSSimulator(device)) { @@ -567,7 +584,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { this.$logger.out("Skipping install."); } - await this.trackActionForPlatform({ action: "Deploy", platform: device.deviceInfo.platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version }); + await this.trackActionForPlatform({ action: constants.TrackActionNames.Deploy, platform: device.deviceInfo.platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version }); }; await this.$devicesService.execute(action, this.getCanExecuteAction(platform, deployOptions)); diff --git a/lib/services/project-templates-service.ts b/lib/services/project-templates-service.ts index d3cb0a0489..32ac27ec57 100644 --- a/lib/services/project-templates-service.ts +++ b/lib/services/project-templates-service.ts @@ -20,6 +20,12 @@ export class ProjectTemplatesService implements IProjectTemplatesService { await this.$analyticsService.track("Template used for project creation", templateName); + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: constants.TrackActionNames.CreateProject, + isForDevice: null, + additionalData: templateName + }); + const realTemplatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir); // this removes dependencies from templates so they are not copied to app folder diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 78883fb1e0..9d63aeae37 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -74,6 +74,12 @@ "integrity": "sha1-3TS72OMv5OdPLj2KwH+KpbRaR6w=", "dev": true }, + "@types/universal-analytics": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/universal-analytics/-/universal-analytics-0.4.1.tgz", + "integrity": "sha512-AZSPpDUEZ4mAgO9geHc62dp/xCLmBJ1yIpbgTq5W/cWcVQsxmU/FyKwYKHXk2hnT9TAmYVFFdAijMrCdYjuHsA==", + "dev": true + }, "abbrev": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", @@ -1838,8 +1844,7 @@ }, "jsbn": { "version": "0.1.1", - "bundled": true, - "optional": true + "bundled": true }, "json-schema": { "version": "0.2.3", @@ -2207,8 +2212,7 @@ }, "tweetnacl": { "version": "0.14.5", - "bundled": true, - "optional": true + "bundled": true }, "uid-number": { "version": "0.0.6", @@ -5164,6 +5168,24 @@ "integrity": "sha1-gGmSYzZl1eX8tNsfs6hi62jp5to=", "dev": true }, + "universal-analytics": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.4.15.tgz", + "integrity": "sha512-9Dt6WBWsHsmv74G+N/rmEgi6KFZxVvQXkVhr0disegeUryybQAUQwMD1l5EtqaOu+hSOGbhL/hPPQYisZIqPRw==", + "requires": { + "async": "1.2.1", + "request": "2.81.0", + "underscore": "1.5.2", + "uuid": "3.0.1" + }, + "dependencies": { + "async": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/async/-/async-1.2.1.tgz", + "integrity": "sha1-pIFqF81f9RbfosdpikUzabl5DeA=" + } + } + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 114d1bc60d..1d2593406f 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "mocha": "node test-scripts/mocha.js", "tsc": "tsc", "tslint": "tslint -p tsconfig.json --type-check", - "test-watch": "node ./dev/tsc-to-mocha-watch.js", - "tslint-fix": "tslint -p tsconfig.json --type-check --fix" + "test-watch": "node ./dev/tsc-to-mocha-watch.js", + "tslint-fix": "tslint -p tsconfig.json --type-check --fix" }, "repository": { "type": "git", @@ -72,6 +72,7 @@ "source-map": "0.5.6", "tabtab": "https://github.com/Icenium/node-tabtab/tarball/master", "temp": "0.8.3", + "universal-analytics": "0.4.15", "uuid": "3.0.1", "winreg": "0.0.17", "ws": "2.2.0", @@ -91,6 +92,7 @@ "@types/request": "0.0.45", "@types/semver": "^5.3.31", "@types/source-map": "0.5.0", + "@types/universal-analytics": "0.4.1", "chai": "4.0.2", "chai-as-promised": "7.0.0", "grunt": "1.0.1", diff --git a/test/plugins-service.ts b/test/plugins-service.ts index 57533401b3..3fdd2c24f2 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -76,7 +76,10 @@ function createTestInjector() { testInjector.register("analyticsService", { trackException: () => { return Promise.resolve(); }, checkConsent: () => { return Promise.resolve(); }, - trackFeature: () => { return Promise.resolve(); } + trackFeature: () => { return Promise.resolve(); }, + trackEventActionInGoogleAnalytics: (data: IEventActionData) => Promise.resolve(), + trackInGoogleAnalytics: (data: IGoogleAnalyticsData) => Promise.resolve(), + trackAcceptFeatureUsage: (settings: { acceptTrackFeatureUsage: boolean }) => Promise.resolve() }); testInjector.register("projectFilesManager", ProjectFilesManager); testInjector.register("pluginVariablesService", { diff --git a/test/project-service.ts b/test/project-service.ts index 4cb3367dce..d0d3090367 100644 --- a/test/project-service.ts +++ b/test/project-service.ts @@ -137,7 +137,10 @@ class ProjectIntegrationTest { this.testInjector.register("fs", FileSystem); this.testInjector.register("projectDataService", ProjectDataServiceLib.ProjectDataService); this.testInjector.register("staticConfig", StaticConfig); - this.testInjector.register("analyticsService", { track: async (): Promise => undefined }); + this.testInjector.register("analyticsService", { + track: async (): Promise => undefined, + trackEventActionInGoogleAnalytics: (data: IEventActionData) => Promise.resolve() + }); this.testInjector.register("npmInstallationManager", NpmInstallationManager); this.testInjector.register("npm", NpmLib.NodePackageManager); diff --git a/test/project-templates-service.ts b/test/project-templates-service.ts index 46e2d1c918..e227a232e3 100644 --- a/test/project-templates-service.ts +++ b/test/project-templates-service.ts @@ -50,7 +50,10 @@ function createTestInjector(configuration?: { shouldNpmInstallThrow: boolean, np injector.register("projectTemplatesService", ProjectTemplatesService); - injector.register("analyticsService", { track: async (): Promise => undefined }); + injector.register("analyticsService", { + track: async (): Promise => undefined, + trackEventActionInGoogleAnalytics: (data: IEventActionData) => Promise.resolve() + }); return injector; } diff --git a/test/services/debug-service.ts b/test/services/debug-service.ts index 8d4887c61f..e3ab301b44 100644 --- a/test/services/debug-service.ts +++ b/test/services/debug-service.ts @@ -93,6 +93,10 @@ describe("debugService", () => { testInjector.register("logger", stubs.LoggerStub); + testInjector.register("analyticsService", { + trackEventActionInGoogleAnalytics: (data: IEventActionData) => Promise.resolve() + }); + return testInjector; }; diff --git a/test/services/livesync-service.ts b/test/services/livesync-service.ts index 0dde25150b..3279bea316 100644 --- a/test/services/livesync-service.ts +++ b/test/services/livesync-service.ts @@ -25,6 +25,7 @@ const createTestInjector = (): IInjector => { }); testInjector.register("pluginsService", {}); + testInjector.register("analyticsService", {}); testInjector.register("injector", testInjector); return testInjector; @@ -44,6 +45,7 @@ class LiveSyncServiceInheritor extends LiveSyncService { $debugService: IDebugService, $errors: IErrors, $debugDataService: IDebugDataService, + $analyticsService: IAnalyticsService, $injector: IInjector) { super( @@ -60,6 +62,7 @@ class LiveSyncServiceInheritor extends LiveSyncService { $debugService, $errors, $debugDataService, + $analyticsService, $injector ); } From 29e2491a1c9837e412f834468d0830f648c5f2ff Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Tue, 5 Sep 2017 13:15:29 +0300 Subject: [PATCH 4/4] Fix hanging commands In case the command is not related to LiveSync, once it finishes its work, CLI process should end. However, this works only on Windows, as the broker child process there is really detached from the parent. On macOS and Linux, the 'ipc' channel between the broker process and CLI, keeps both alive. So use the 'dispose' method, which is called at the end of command execution and disconnect the broker process. In case we are in LiveSync case, set shouldDispose to false and this way the broker will be kept alive. Once Ctrl + C is used to end the CLI process, send the finish message to broker and it will disconnect itself from CLI. --- lib/commands/run.ts | 1 - lib/common | 2 +- .../analytics/analytics-broker-process.ts | 8 ++++++-- lib/services/analytics/analytics-service.ts | 17 +++++++++++++---- .../analytics/eqatec-analytics-provider.ts | 4 ++++ .../analytics/google-analytics-provider.ts | 2 +- .../livesync/livesync-command-helper.ts | 2 ++ lib/services/test-execution-service.ts | 2 ++ npm-shrinkwrap.json | 2 +- 9 files changed, 30 insertions(+), 10 deletions(-) diff --git a/lib/commands/run.ts b/lib/commands/run.ts index da4da5148b..505552f685 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -5,7 +5,6 @@ export class RunCommandBase implements ICommand { public platform: string; constructor(protected $platformService: IPlatformService, - protected $liveSyncService: ILiveSyncService, protected $projectData: IProjectData, protected $options: IOptions, protected $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, diff --git a/lib/common b/lib/common index 7072607b66..804e28c137 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 7072607b66f0394699b6bb36a5c66f366f3965fa +Subproject commit 804e28c137442922609caced942dee3d6512523b diff --git a/lib/services/analytics/analytics-broker-process.ts b/lib/services/analytics/analytics-broker-process.ts index cfcf9ed406..78769ae8ea 100644 --- a/lib/services/analytics/analytics-broker-process.ts +++ b/lib/services/analytics/analytics-broker-process.ts @@ -25,15 +25,19 @@ const finishTracking = async (data?: ITrackingInformation) => { sentFinishMsg = true; data = data || { type: TrackingTypes.Finish }; + const action = async () => { + await sendDataForTracking(data); + process.disconnect(); + }; if (receivedFinishMsg) { - await sendDataForTracking(data); + await action(); } else { // In case we've got here without receiving "finish" message from parent (receivedFinishMsg is false) // there might be various reasons, but most probably the parent is dead. // However, there's no guarantee that we've received all messages. So wait some time before sending finish message to children. setTimeout(async () => { - await sendDataForTracking(data); + await action(); }, 1000); } } diff --git a/lib/services/analytics/analytics-service.ts b/lib/services/analytics/analytics-service.ts index d7d232a86c..a15f7f7583 100644 --- a/lib/services/analytics/analytics-service.ts +++ b/lib/services/analytics/analytics-service.ts @@ -5,8 +5,9 @@ import { cache } from "../../common/decorators"; import { isInteractive } from '../../common/helpers'; import { DeviceTypes, AnalyticsClients } from "../../common/constants"; -export class AnalyticsService extends AnalyticsServiceBase implements IAnalyticsService { +export class AnalyticsService extends AnalyticsServiceBase { private static ANALYTICS_BROKER_START_TIMEOUT = 30 * 1000; + private brokerProcess: ChildProcess; constructor(protected $logger: ILogger, protected $options: IOptions, @@ -56,7 +57,7 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics gaSettings.customDimensions = gaSettings.customDimensions || {}; gaSettings.customDimensions[GoogleAnalyticsCustomDimensions.client] = this.$options.analyticsClient || (isInteractive() ? AnalyticsClients.Cli : AnalyticsClients.Unknown); - const googleAnalyticsData: IGoogleAnalyticsTrackingInformation = _.merge({ type: TrackingTypes.GoogleAnalyticsData }, gaSettings, { category: AnalyticsClients.Cli }); + const googleAnalyticsData: IGoogleAnalyticsTrackingInformation = _.merge({ type: TrackingTypes.GoogleAnalyticsData, category: AnalyticsClients.Cli }, gaSettings); return this.sendMessageToBroker(googleAnalyticsData); } } @@ -69,9 +70,9 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics let label: string = ""; label = this.addDataToLabel(label, platform); + // In some cases (like in case action is Build and platform is Android), we do not know if the deviceType is emulator or device. + // Just exclude the device_type in this case. if (isForDevice !== null) { - // In case action is Build and platform is Android, we do not know if the deviceType is emulator or device. - // Just exclude the device_type in this case. const deviceType = isForDevice ? DeviceTypes.Device : (this.$mobileHelper.isAndroidPlatform(platform) ? DeviceTypes.Emulator : DeviceTypes.Simulator); label = this.addDataToLabel(label, deviceType); } @@ -102,6 +103,12 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics await this.trackInGoogleAnalytics(googleAnalyticsEventData); } + public dispose(): void { + if (this.brokerProcess && this.shouldDisposeInstance) { + this.brokerProcess.disconnect(); + } + } + private addDataToLabel(label: string, newData: string): string { if (newData && label) { return `${label}_${newData}`; @@ -156,6 +163,8 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics }); }); + this.brokerProcess = broker; + resolve(broker); } } diff --git a/lib/services/analytics/eqatec-analytics-provider.ts b/lib/services/analytics/eqatec-analytics-provider.ts index e9539372a9..e2b2f43163 100644 --- a/lib/services/analytics/eqatec-analytics-provider.ts +++ b/lib/services/analytics/eqatec-analytics-provider.ts @@ -51,6 +51,10 @@ export class EqatecAnalyticsProvider extends AnalyticsServiceBase implements IAn this.tryStopEqatecMonitors(); } + public dispose(): void { + // Intentionally left blank. + } + } $injector.register("eqatecAnalyticsProvider", EqatecAnalyticsProvider); diff --git a/lib/services/analytics/google-analytics-provider.ts b/lib/services/analytics/google-analytics-provider.ts index 2e8055ca92..1bb8d9fe13 100644 --- a/lib/services/analytics/google-analytics-provider.ts +++ b/lib/services/analytics/google-analytics-provider.ts @@ -109,7 +109,7 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { private getMacOSReleaseVersion(osRelease: string): string { // https://en.wikipedia.org/wiki/Darwin_(operating_system)#Release_history - // Each macOS version is labeled 10., where it looks like is taken from the major version returned by os.release() (16.x.x for example) and substituting 4 from it. + // Each macOS version is labeled 10., where it looks like is taken from the major version returned by os.release() (16.x.x for example) and subtracting 4 from it. // So the version becomes "10.12" in this case. // Could be improved by spawning `system_profiler SPSoftwareDataType` and getting the System Version line from the result. const majorVersion = osRelease && _.first(osRelease.split(".")); diff --git a/lib/services/livesync/livesync-command-helper.ts b/lib/services/livesync/livesync-command-helper.ts index fae89af3c9..54be3c7a19 100644 --- a/lib/services/livesync/livesync-command-helper.ts +++ b/lib/services/livesync/livesync-command-helper.ts @@ -7,7 +7,9 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { private $iosDeviceOperations: IIOSDeviceOperations, private $mobileHelper: Mobile.IMobileHelper, private $platformsData: IPlatformsData, + private $analyticsService: IAnalyticsService, private $errors: IErrors) { + this.$analyticsService.setShouldDispose(this.$options.justlaunch || !this.$options.watch); } public getPlatformsForOperation(platform: string): string[] { diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index b3d4221d26..318032c65b 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -26,7 +26,9 @@ class TestExecutionService implements ITestExecutionService { private $errors: IErrors, private $debugService: IDebugService, private $devicesService: Mobile.IDevicesService, + private $analyticsService: IAnalyticsService, private $childProcess: IChildProcess) { + this.$analyticsService.setShouldDispose(this.$options.justlaunch || !this.$options.watch); } public platform: string; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9d63aeae37..eb666497e4 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "nativescript", - "version": "3.2.0", + "version": "3.3.0", "lockfileVersion": 1, "requires": true, "dependencies": {