diff --git a/lib/commands/preview.ts b/lib/commands/preview.ts index 7d8a4af6ec..a7f613b1d5 100644 --- a/lib/commands/preview.ts +++ b/lib/commands/preview.ts @@ -4,7 +4,8 @@ export class PreviewCommand implements ICommand { public allowedParameters: ICommandParameter[] = []; private static MIN_SUPPORTED_WEBPACK_VERSION = "0.17.0"; - constructor(private $bundleValidatorHelper: IBundleValidatorHelper, + constructor(private $analyticsService: IAnalyticsService, + private $bundleValidatorHelper: IBundleValidatorHelper, private $errors: IErrors, private $liveSyncService: ILiveSyncService, private $logger: ILogger, @@ -12,7 +13,9 @@ export class PreviewCommand implements ICommand { private $projectData: IProjectData, private $options: IOptions, private $previewAppLogProvider: IPreviewAppLogProvider, - private $previewQrCodeService: IPreviewQrCodeService) { } + private $previewQrCodeService: IPreviewQrCodeService) { + this.$analyticsService.setShouldDispose(this.$options.justlaunch || !this.$options.watch); + } public async execute(): Promise { this.$previewAppLogProvider.on(DEVICE_LOG_EVENT_NAME, (deviceId: string, message: string) => { diff --git a/lib/commands/run.ts b/lib/commands/run.ts index dfc1f98fc8..5e34cfe578 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -6,7 +6,9 @@ export class RunCommandBase implements ICommand { private liveSyncCommandHelperAdditionalOptions: ILiveSyncCommandHelperAdditionalOptions = {}; public platform: string; - constructor(private $projectData: IProjectData, + constructor( + private $analyticsService: IAnalyticsService, + private $projectData: IProjectData, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $errors: IErrors, private $hostInfo: IHostInfo, @@ -15,6 +17,7 @@ export class RunCommandBase implements ICommand { public allowedParameters: ICommandParameter[] = []; public async execute(args: string[]): Promise { + await this.$analyticsService.trackPreviewAppData(this.platform, this.$projectData.projectDir); return this.$liveSyncCommandHelper.executeCommandLiveSync(this.platform, this.liveSyncCommandHelperAdditionalOptions); } diff --git a/lib/common/declarations.d.ts b/lib/common/declarations.d.ts index 6739078fea..cd9c9e5b94 100644 --- a/lib/common/declarations.d.ts +++ b/lib/common/declarations.d.ts @@ -203,6 +203,11 @@ declare const enum TrackingTypes { */ GoogleAnalyticsData = "googleAnalyticsData", + /** + * Defines that the broker process should get and track the data from preview app to Google Analytics + */ + PreviewAppData = "PreviewAppData", + /** * Defines that all information has been sent and no more data will be tracked in current session. */ @@ -690,6 +695,11 @@ interface IAnalyticsService { */ trackEventActionInGoogleAnalytics(data: IEventActionData): Promise; + /** + * Tracks preview's app data to Google Analytics project. + */ + trackPreviewAppData(platform: string, projectDir: string): Promise + /** * Defines if the instance should be disposed. * @param {boolean} shouldDispose Defines if the instance should be disposed and the child processes should be disconnected. diff --git a/lib/common/definitions/google-analytics.d.ts b/lib/common/definitions/google-analytics.d.ts index 4a99d9a7ae..d5c1893530 100644 --- a/lib/common/definitions/google-analytics.d.ts +++ b/lib/common/definitions/google-analytics.d.ts @@ -13,6 +13,11 @@ interface IGoogleAnalyticsData { customDimensions?: IStringDictionary; } +interface IPreviewAppGoogleAnalyticsData { + platform: string; + additionalData?: string; +} + /** * Describes information about event that should be tracked. */ diff --git a/lib/common/definitions/mobile.d.ts b/lib/common/definitions/mobile.d.ts index 666a5ac5cc..9a76d4eae7 100644 --- a/lib/common/definitions/mobile.d.ts +++ b/lib/common/definitions/mobile.d.ts @@ -320,6 +320,7 @@ declare module Mobile { interface IDeviceFileSystem { listFiles(devicePath: string, appIdentifier?: string): Promise; getFile(deviceFilePath: string, appIdentifier: string, outputFilePath?: string): Promise; + getFileContent(deviceFilePath: string, appIdentifier: string): Promise; putFile(localFilePath: string, deviceFilePath: string, appIdentifier: string): Promise; deleteFile(deviceFilePath: string, appIdentifier: string): Promise; transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise; diff --git a/lib/common/mobile/android/android-device-file-system.ts b/lib/common/mobile/android/android-device-file-system.ts index e87f0f7608..ef523a6960 100644 --- a/lib/common/mobile/android/android-device-file-system.ts +++ b/lib/common/mobile/android/android-device-file-system.ts @@ -48,6 +48,11 @@ export class AndroidDeviceFileSystem implements Mobile.IDeviceFileSystem { } } + public async getFileContent(deviceFilePath: string, appIdentifier: string): Promise { + const result = await this.adb.executeShellCommand(["cat", deviceFilePath]); + return result; + } + public async putFile(localFilePath: string, deviceFilePath: string, appIdentifier: string): Promise { await this.adb.pushFile(localFilePath, deviceFilePath); } diff --git a/lib/common/mobile/ios/device/ios-device-file-system.ts b/lib/common/mobile/ios/device/ios-device-file-system.ts index 4c0475ea78..d5637751cf 100644 --- a/lib/common/mobile/ios/device/ios-device-file-system.ts +++ b/lib/common/mobile/ios/device/ios-device-file-system.ts @@ -23,15 +23,19 @@ export class IOSDeviceFileSystem implements Mobile.IDeviceFileSystem { } public async getFile(deviceFilePath: string, appIdentifier: string, outputFilePath?: string): Promise { - if (!outputFilePath) { - const result = await this.$iosDeviceOperations.readFiles([{ deviceId: this.device.deviceInfo.identifier, path: deviceFilePath, appId: appIdentifier }]); - const response = result[this.device.deviceInfo.identifier][0]; - if (response) { - this.$logger.out(response.response); - } - } else { + if (outputFilePath) { await this.$iosDeviceOperations.downloadFiles([{ appId: appIdentifier, deviceId: this.device.deviceInfo.identifier, source: deviceFilePath, destination: outputFilePath }]); + return; } + + const fileContent = await this.getFileContent(deviceFilePath, appIdentifier); + this.$logger.out(fileContent); + } + + public async getFileContent(deviceFilePath: string, appIdentifier: string): Promise { + const result = await this.$iosDeviceOperations.readFiles([{ deviceId: this.device.deviceInfo.identifier, path: deviceFilePath, appId: appIdentifier }]); + const response = result[this.device.deviceInfo.identifier][0]; + return response.response; } public async putFile(localFilePath: string, deviceFilePath: string, appIdentifier: string): Promise { diff --git a/lib/common/mobile/ios/simulator/ios-simulator-file-system.ts b/lib/common/mobile/ios/simulator/ios-simulator-file-system.ts index 15e1f1159f..a4f0b0d5ab 100644 --- a/lib/common/mobile/ios/simulator/ios-simulator-file-system.ts +++ b/lib/common/mobile/ios/simulator/ios-simulator-file-system.ts @@ -16,6 +16,11 @@ export class IOSSimulatorFileSystem implements Mobile.IDeviceFileSystem { } } + public async getFileContent(deviceFilePath: string, appIdentifier: string): Promise { + const result = this.$fs.readText(deviceFilePath); + return result; + } + public async putFile(localFilePath: string, deviceFilePath: string, appIdentifier: string): Promise { shelljs.cp("-f", localFilePath, deviceFilePath); } @@ -27,7 +32,7 @@ export class IOSSimulatorFileSystem implements Mobile.IDeviceFileSystem { public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { await Promise.all( _.map(localToDevicePaths, localToDevicePathData => this.transferFile(localToDevicePathData.getLocalPath(), localToDevicePathData.getDevicePath()) - )); + )); return localToDevicePaths; } diff --git a/lib/constants.ts b/lib/constants.ts index d783a2b1fe..2e82a9c2ea 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -156,7 +156,8 @@ export const enum TrackActionNames { CheckEnvironmentRequirements = "Check Environment Requirements", Options = "Options", AcceptTracking = "Accept Tracking", - Performance = "Performance" + Performance = "Performance", + PreviewAppData = "Preview App Data" } export const AnalyticsEventLabelDelimiter = "__"; diff --git a/lib/services/analytics/analytics-broker-process.ts b/lib/services/analytics/analytics-broker-process.ts index 6efd6d925f..a687fd87df 100644 --- a/lib/services/analytics/analytics-broker-process.ts +++ b/lib/services/analytics/analytics-broker-process.ts @@ -54,8 +54,35 @@ const finishTracking = async (data?: ITrackingInformation) => { } }; +const trackPreviewAppData = async (data: any) => { + const mobileHelper = $injector.resolve("mobileHelper"); + const devicesService = $injector.resolve("devicesService"); + await devicesService.initialize({ platform: data.platform, skipDeviceDetectionInterval: true, skipEmulatorStart: true }); + + const devices = await devicesService.getDevicesForPlatform(data.platform); + _.each(devices, async (device: Mobile.IDevice) => { + try { + let previewAppFilePath = null; + if (mobileHelper.isAndroidPlatform(device.deviceInfo.platform)) { + previewAppFilePath = "/sdcard/org.nativescript.preview/device.json"; + } else if (mobileHelper.isiOSPlatform(device.deviceInfo.platform)) { + previewAppFilePath = "Documents/device.json"; + } + + const previewAppFileContent = await device.fileSystem.getFileContent(previewAppFilePath, "org.nativescript.preview"); + const previewAppDeviceId = JSON.parse(previewAppFileContent).id; + data.label += `_${previewAppDeviceId}`; + + analyticsLoggingService.logData({ message: `analytics-broker-process will send the data from preview app: ${data}` }); + await sendDataForTracking(data); + } catch (err) { + // ignore the error + } + }); +}; + process.on("message", async (data: ITrackingInformation) => { - analyticsLoggingService.logData({ message: `analytics-broker-process received message of type: ${data.type}` }); + analyticsLoggingService.logData({ message: `analytics-broker-process received message of type: ${JSON.stringify(data)}` }); if (data.type === TrackingTypes.Finish) { receivedFinishMsg = true; @@ -63,6 +90,11 @@ process.on("message", async (data: ITrackingInformation) => { return; } + if (data.type === TrackingTypes.PreviewAppData) { + await trackPreviewAppData(data); + return; + } + await sendDataForTracking(data); }); diff --git a/lib/services/analytics/analytics-service.ts b/lib/services/analytics/analytics-service.ts index 58206abb17..3871e75752 100644 --- a/lib/services/analytics/analytics-service.ts +++ b/lib/services/analytics/analytics-service.ts @@ -96,7 +96,6 @@ export class AnalyticsService implements IAnalyticsService, IDisposable { const platform = device ? device.deviceInfo.platform : data.platform; const normalizedPlatform = platform ? this.$mobileHelper.normalizePlatformName(platform) : platform; const isForDevice = device ? !device.isEmulator : data.isForDevice; - let label: string = ""; label = this.addDataToLabel(label, normalizedPlatform); @@ -129,6 +128,25 @@ export class AnalyticsService implements IAnalyticsService, IDisposable { await this.trackInGoogleAnalytics(googleAnalyticsEventData); } + public async trackPreviewAppData(platform: string, projectDir: string): Promise { + const customDimensions: IStringDictionary = {}; + this.setProjectRelatedCustomDimensions(customDimensions, projectDir); + + let label: string = ""; + label = this.addDataToLabel(label, this.$mobileHelper.normalizePlatformName(platform)); + + const eventActionData = { + googleAnalyticsDataType: GoogleAnalyticsDataType.Event, + action: TrackActionNames.PreviewAppData, + platform, + label, + customDimensions, + type: TrackingTypes.PreviewAppData + }; + + await this.trackInGoogleAnalytics(eventActionData); + } + private forcefullyTrackInGoogleAnalytics(gaSettings: IGoogleAnalyticsData): Promise { gaSettings.customDimensions = gaSettings.customDimensions || {}; gaSettings.customDimensions[GoogleAnalyticsCustomDimensions.client] = this.$options.analyticsClient || (isInteractive() ? AnalyticsClients.Cli : AnalyticsClients.Unknown); diff --git a/lib/services/analytics/analytics.d.ts b/lib/services/analytics/analytics.d.ts index 1966914fdd..6517ea016f 100644 --- a/lib/services/analytics/analytics.d.ts +++ b/lib/services/analytics/analytics.d.ts @@ -37,6 +37,8 @@ interface IAnalyticsBroker { interface IGoogleAnalyticsTrackingInformation extends IGoogleAnalyticsData, ITrackingInformation { } +interface IPreviewAppTrackingInformation extends IPreviewAppGoogleAnalyticsData, ITrackingInformation { } + /** * Describes methods required to track in Google Analytics. */ diff --git a/lib/services/livesync/playground/preview-app-livesync-service.ts b/lib/services/livesync/playground/preview-app-livesync-service.ts index 286e2da588..b1d2d6537d 100644 --- a/lib/services/livesync/playground/preview-app-livesync-service.ts +++ b/lib/services/livesync/playground/preview-app-livesync-service.ts @@ -1,6 +1,6 @@ import * as path from "path"; import { Device, FilesPayload } from "nativescript-preview-sdk"; -import { APP_RESOURCES_FOLDER_NAME, APP_FOLDER_NAME } from "../../../constants"; +import { APP_RESOURCES_FOLDER_NAME, APP_FOLDER_NAME, TrackActionNames } from "../../../constants"; import { PreviewAppLiveSyncEvents } from "./preview-app-constants"; import { HmrConstants } from "../../../common/constants"; import { stringify } from "../../../common/helpers"; @@ -12,6 +12,7 @@ export class PreviewAppLiveSyncService extends EventEmitter implements IPreviewA private deviceInitializationPromise: IDictionary> = {}; constructor( + private $analyticsService: IAnalyticsService, private $errors: IErrors, private $hooksService: IHooksService, private $logger: ILogger, @@ -37,6 +38,14 @@ export class PreviewAppLiveSyncService extends EventEmitter implements IPreviewA return this.deviceInitializationPromise[device.id]; } + if (device.uniqueId) { + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: TrackActionNames.PreviewAppData, + platform: device.platform, + additionalData: device.uniqueId + }); + } + this.deviceInitializationPromise[device.id] = this.getInitialFilesForDevice(data, device); try { const payloads = await this.deviceInitializationPromise[device.id]; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 332d5a99d6..ce0273a34c 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -4490,6 +4490,19 @@ "supports-color": "^5.3.0" } }, + "cli-table": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "requires": { + "colors": "1.0.3" + } + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -4825,9 +4838,9 @@ } }, "nativescript-preview-sdk": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/nativescript-preview-sdk/-/nativescript-preview-sdk-0.3.2.tgz", - "integrity": "sha512-ydFlM7PgLT5sboiTOmoA0+ttPUys6FJpjG5CMgnSMTVyJ9AjQYMqi4qUT7mdO3xlZOPAyYdKH3AbpUI312qklQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/nativescript-preview-sdk/-/nativescript-preview-sdk-0.3.3.tgz", + "integrity": "sha512-QHi4SbDblN+dSZCIztx1xVdrB4cP911OXV92REIVOqDHXyAFYUZeWu5DH0/3IHdC9fnMgXMeJ0WZEpgGgieqtQ==", "requires": { "@types/axios": "0.14.0", "@types/pubnub": "4.0.2", diff --git a/package.json b/package.json index 544f953049..4a0078f6a4 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "mkdirp": "0.5.1", "mute-stream": "0.0.5", "nativescript-doctor": "1.8.1", - "nativescript-preview-sdk": "0.3.2", + "nativescript-preview-sdk": "0.3.3", "open": "0.0.5", "ora": "2.0.0", "osenv": "0.1.3", diff --git a/test/services/playground/preview-app-livesync-service.ts b/test/services/playground/preview-app-livesync-service.ts index ccbf24b999..42f097a24e 100644 --- a/test/services/playground/preview-app-livesync-service.ts +++ b/test/services/playground/preview-app-livesync-service.ts @@ -158,6 +158,9 @@ function createTestInjector(options?: { getConnectedDevices: () => [deviceMockData] }); injector.register("previewAppFilesService", PreviewAppFilesService); + injector.register("analyticsService", { + trackEventActionInGoogleAnalytics: () => ({}) + }); return injector; } diff --git a/test/services/playground/preview-app-plugins-service.ts b/test/services/playground/preview-app-plugins-service.ts index 776f72d5d3..5e239819cc 100644 --- a/test/services/playground/preview-app-plugins-service.ts +++ b/test/services/playground/preview-app-plugins-service.ts @@ -59,7 +59,8 @@ function createDevice(plugins: string): Device { previewAppVersion: "28.0.0", runtimeVersion: "4.3.0", plugins, - pluginsExpanded: false + pluginsExpanded: false, + uniqueId: "testId" }; } diff --git a/test/services/preview-devices-service.ts b/test/services/preview-devices-service.ts index 6ae78312b4..886095d78e 100644 --- a/test/services/preview-devices-service.ts +++ b/test/services/preview-devices-service.ts @@ -29,7 +29,8 @@ function createDevice(id: string): Device { name: "my test name", osVersion: "10.0.0", previewAppVersion: "19.0.0", - runtimeVersion: "5.0.0" + runtimeVersion: "5.0.0", + uniqueId: "testUniqueId" }; }