diff --git a/lib/constants.ts b/lib/constants.ts index 877328afd0..140def1c34 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -35,6 +35,8 @@ export const AWAIT_NOTIFICATION_TIMEOUT_SECONDS = 9; export const SRC_DIR = "src"; export const MAIN_DIR = "main"; export const ASSETS_DIR = "assets"; +export const ANDROID_ANALYTICS_DATA_DIR = "analytics"; +export const ANDROID_ANALYTICS_DATA_FILE = "build-statistics.json"; export const MANIFEST_FILE_NAME = "AndroidManifest.xml"; export const APP_GRADLE_FILE_NAME = "app.gradle"; export const INFO_PLIST_FILE_NAME = "Info.plist"; @@ -188,7 +190,8 @@ export const enum TrackActionNames { PreviewAppData = "Preview App Data", UninstallCLI = "Uninstall CLI", UsingRuntimeVersion = "Using Runtime Version", - AddPlatform = "Add Platform" + AddPlatform = "Add Platform", + UsingKotlin = "Using Kotlin" } export const AnalyticsEventLabelDelimiter = "__"; diff --git a/lib/definitions/gradle.d.ts b/lib/definitions/gradle.d.ts index 5e40bb1ed6..b4321348e1 100644 --- a/lib/definitions/gradle.d.ts +++ b/lib/definitions/gradle.d.ts @@ -15,6 +15,6 @@ interface IGradleBuildService { } interface IGradleBuildArgsService { - getBuildTaskArgs(buildData: IAndroidBuildData): string[]; + getBuildTaskArgs(buildData: IAndroidBuildData): Promise; getCleanTaskArgs(buildData: IAndroidBuildData): string[]; } diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index 04c6a2363b..d8efcf13e5 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -25,7 +25,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private $androidResourcesMigrationService: IAndroidResourcesMigrationService, private $filesHashService: IFilesHashService, private $gradleCommandService: IGradleCommandService, - private $gradleBuildService: IGradleBuildService) { + private $gradleBuildService: IGradleBuildService, + private $analyticsService: IAnalyticsService) { super($fs, $projectDataService); } @@ -235,6 +236,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject const outputPath = platformData.getBuildOutputPath(buildData); await this.$filesHashService.saveHashesForProject(this._platformData, outputPath); + await this.trackKotlinUsage(projectRoot); } public async buildForDeploy(projectRoot: string, projectData: IProjectData, buildData?: IAndroidBuildData): Promise { @@ -444,6 +446,39 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject }); } } + + private async trackKotlinUsage(projectRoot: string): Promise { + const buildStatistics = this.tryGetAndroidBuildStatistics(projectRoot); + + try { + if (buildStatistics && buildStatistics.kotlinUsage) { + const analyticsDelimiter = constants.AnalyticsEventLabelDelimiter; + const hasUseKotlinPropertyInAppData = `hasUseKotlinPropertyInApp${analyticsDelimiter}${buildStatistics.kotlinUsage.hasUseKotlinPropertyInApp}`; + const hasKotlinRuntimeClassesData = `hasKotlinRuntimeClasses${analyticsDelimiter}${buildStatistics.kotlinUsage.hasKotlinRuntimeClasses}`; + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: constants.TrackActionNames.UsingKotlin, + additionalData: `${hasUseKotlinPropertyInAppData}${analyticsDelimiter}${hasKotlinRuntimeClassesData}` + }); + } + } catch (e) { + this.$logger.trace(`Failed to track android build statistics. Error is: ${e.message}`); + } + } + + private tryGetAndroidBuildStatistics(projectRoot: string): Object { + const staticsFilePath = path.join(projectRoot, constants.ANDROID_ANALYTICS_DATA_DIR, constants.ANDROID_ANALYTICS_DATA_FILE); + let buildStatistics; + + if (this.$fs.exists(staticsFilePath)) { + try { + buildStatistics = this.$fs.readJson(staticsFilePath); + } catch (e) { + this.$logger.trace(`Unable to read android build statistics file. Error is ${e.message}`); + } + } + + return buildStatistics; + } } $injector.register("androidProjectService", AndroidProjectService); diff --git a/lib/services/android/gradle-build-args-service.ts b/lib/services/android/gradle-build-args-service.ts index 490283a43e..be3c354079 100644 --- a/lib/services/android/gradle-build-args-service.ts +++ b/lib/services/android/gradle-build-args-service.ts @@ -3,12 +3,18 @@ import { Configurations } from "../../common/constants"; export class GradleBuildArgsService implements IGradleBuildArgsService { constructor(private $androidToolsInfo: IAndroidToolsInfo, + private $analyticsService: IAnalyticsService, + private $staticConfig: Config.IStaticConfig, private $logger: ILogger) { } - public getBuildTaskArgs(buildData: IAndroidBuildData): string[] { + public async getBuildTaskArgs(buildData: IAndroidBuildData): Promise { const args = this.getBaseTaskArgs(buildData); args.unshift(this.getBuildTaskName(buildData)); + if (await this.$analyticsService.isEnabled(this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME)) { + args.push("-PgatherAnalyticsData=true"); + } + return args; } diff --git a/lib/services/android/gradle-build-service.ts b/lib/services/android/gradle-build-service.ts index 1f34367b5c..740d81aecc 100644 --- a/lib/services/android/gradle-build-service.ts +++ b/lib/services/android/gradle-build-service.ts @@ -10,7 +10,7 @@ export class GradleBuildService extends EventEmitter implements IGradleBuildServ ) { super(); } public async buildProject(projectRoot: string, buildData: IAndroidBuildData): Promise { - const buildTaskArgs = this.$gradleBuildArgsService.getBuildTaskArgs(buildData); + const buildTaskArgs = await this.$gradleBuildArgsService.getBuildTaskArgs(buildData); const spawnOptions = { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }; const gradleCommandOptions = { cwd: projectRoot, message: "Gradle build...", stdio: buildData.buildOutputStdio, spawnOptions }; diff --git a/test/cocoapods-service.ts b/test/cocoapods-service.ts index 52ff704b3b..61edfcb44a 100644 --- a/test/cocoapods-service.ts +++ b/test/cocoapods-service.ts @@ -40,6 +40,10 @@ function changeNewLineCharacter(input: string): string { } describe("Cocoapods service", () => { + if (require("os").platform() === "win32") { + console.log("Skipping 'Cocoapods service' tests. They can work only on macOS and Linux"); + return; + } const nativeProjectPath = "nativeProjectPath"; const mockPluginData: any = { name: "plugin1", diff --git a/test/services/android-project-service.ts b/test/services/android-project-service.ts index 96bdc0a128..d25b51982f 100644 --- a/test/services/android-project-service.ts +++ b/test/services/android-project-service.ts @@ -41,7 +41,8 @@ const createTestInjector = (): IInjector => { testInjector.register("gradleCommandService", GradleCommandService); testInjector.register("gradleBuildService", GradleBuildService); testInjector.register("gradleBuildArgsService", GradleBuildArgsService); - + testInjector.register("analyticsService", stubs.AnalyticsService); + testInjector.register("staticConfig", {TRACK_FEATURE_USAGE_SETTING_NAME: "TrackFeatureUsage"}); return testInjector; }; diff --git a/test/services/android/gradle-build-args-service.ts b/test/services/android/gradle-build-args-service.ts index 7c23bdb2cd..e9c1a4428a 100644 --- a/test/services/android/gradle-build-args-service.ts +++ b/test/services/android/gradle-build-args-service.ts @@ -1,6 +1,9 @@ import { Yok } from "../../../lib/common/yok"; import { GradleBuildArgsService } from "../../../lib/services/android/gradle-build-args-service"; +import * as stubs from "../../stubs"; import { assert } from "chai"; +import * as temp from "temp"; +temp.track(); function createTestInjector(): IInjector { const injector = new Yok(); @@ -14,13 +17,15 @@ function createTestInjector(): IInjector { }); injector.register("logger", {}); injector.register("gradleBuildArgsService", GradleBuildArgsService); + injector.register("analyticsService", stubs.AnalyticsService); + injector.register("staticConfig", {TRACK_FEATURE_USAGE_SETTING_NAME: "TrackFeatureUsage"}); return injector; } -function executeTests(testCases: any[], testFunction: (gradleBuildArgsService: IGradleBuildArgsService, buildData: IAndroidBuildData) => string[]) { - _.each(testCases, testCase => { - it(testCase.name, () => { +async function executeTests(testCases: any[], testFunction: (gradleBuildArgsService: IGradleBuildArgsService, buildData: IAndroidBuildData) => Promise) { + for (const testCase of testCases) { + it(testCase.name, async () => { const injector = createTestInjector(); if (testCase.logLevel) { const logger = injector.resolve("logger"); @@ -28,29 +33,29 @@ function executeTests(testCases: any[], testFunction: (gradleBuildArgsService: I } const gradleBuildArgsService = injector.resolve("gradleBuildArgsService"); - const args = testFunction(gradleBuildArgsService, testCase.buildConfig); + const args = await testFunction(gradleBuildArgsService, testCase.buildConfig); assert.deepEqual(args, testCase.expectedResult); }); - }); + } } - +const ksPath = temp.path({ prefix: "ksPath" }); const expectedInfoLoggingArgs = ["--quiet"]; const expectedTraceLoggingArgs = ["--stacktrace", "--debug"]; const expectedDebugBuildArgs = ["-PcompileSdk=android-28", "-PtargetSdk=26", "-PbuildToolsVersion=my-build-tools-version", "-PgenerateTypings=true"]; -const expectedReleaseBuildArgs = expectedDebugBuildArgs.concat(["-Prelease", "-PksPath=/my/key/store/path", +const expectedReleaseBuildArgs = expectedDebugBuildArgs.concat(["-Prelease", `-PksPath=${ksPath}`, "-Palias=keyStoreAlias", "-Ppassword=keyStoreAliasPassword", "-PksPassword=keyStorePassword"]); const releaseBuildConfig = { release: true, - keyStorePath: "/my/key/store/path", + keyStorePath: ksPath, keyStoreAlias: "keyStoreAlias", keyStoreAliasPassword: "keyStoreAliasPassword", keyStorePassword: "keyStorePassword" }; describe("GradleBuildArgsService", () => { - describe("getBuildTaskArgs", () => { + describe("getBuildTaskArgs", async () => { const testCases = [ { name: "should return correct args for debug build with info log", @@ -102,10 +107,10 @@ describe("GradleBuildArgsService", () => { } ]; - executeTests(testCases, (gradleBuildArgsService: IGradleBuildArgsService, buildData: IAndroidBuildData) => gradleBuildArgsService.getBuildTaskArgs(buildData)); + await executeTests(testCases, (gradleBuildArgsService: IGradleBuildArgsService, buildData: IAndroidBuildData) => gradleBuildArgsService.getBuildTaskArgs(buildData)); }); - describe("getCleanTaskArgs", () => { + describe("getCleanTaskArgs", async () => { const testCases = [ { name: "should return correct args for debug clean build with info log", @@ -157,6 +162,6 @@ describe("GradleBuildArgsService", () => { } ]; - executeTests(testCases, (gradleBuildArgsService: IGradleBuildArgsService, buildData: IAndroidBuildData) => gradleBuildArgsService.getCleanTaskArgs(buildData)); + await executeTests(testCases, (gradleBuildArgsService: IGradleBuildArgsService, buildData: IAndroidBuildData) => Promise.resolve(gradleBuildArgsService.getCleanTaskArgs(buildData))); }); }); diff --git a/test/services/ios/export-options-plist-service.ts b/test/services/ios/export-options-plist-service.ts index c6bd2cb96c..66abec28e3 100644 --- a/test/services/ios/export-options-plist-service.ts +++ b/test/services/ios/export-options-plist-service.ts @@ -1,7 +1,6 @@ import { Yok } from "../../../lib/common/yok"; import { ExportOptionsPlistService } from "../../../lib/services/ios/export-options-plist-service"; import { assert } from "chai"; -import { EOL } from "os"; let actualPlistTemplate: string = null; const projectName = "myProjectName"; @@ -54,7 +53,7 @@ describe("ExportOptionsPlistService", () => { const projectData = { projectName, projectIdentifiers: { ios: "org.nativescript.myTestApp" }}; exportOptionsPlistService.createDevelopmentExportOptionsPlist(archivePath, projectData, testCase.buildConfig); - const template = actualPlistTemplate.split(EOL).join(" "); + const template = actualPlistTemplate.split("\n").join(" "); assert.isTrue(template.indexOf(`method ${provisionType}`) > 0); assert.isTrue(template.indexOf("uploadBitcode ") > 0); assert.isTrue(template.indexOf("compileBitcode ") > 0); @@ -97,7 +96,7 @@ describe("ExportOptionsPlistService", () => { const projectData = { projectName, projectIdentifiers: { ios: "org.nativescript.myTestApp" }}; exportOptionsPlistService.createDistributionExportOptionsPlist(projectRoot, projectData, testCase.buildConfig); - const template = actualPlistTemplate.split(EOL).join(" "); + const template = actualPlistTemplate.split("\n").join(" "); assert.isTrue(template.indexOf("method app-store") > 0); assert.isTrue(template.indexOf("uploadBitcode ") > 0); assert.isTrue(template.indexOf("compileBitcode ") > 0); diff --git a/test/services/playground/preview-app-livesync-service.ts b/test/services/playground/preview-app-livesync-service.ts index 229a73143e..343cfd514e 100644 --- a/test/services/playground/preview-app-livesync-service.ts +++ b/test/services/playground/preview-app-livesync-service.ts @@ -59,12 +59,12 @@ const deviceMockData = { platform: normalizedPlatformName }; const defaultProjectFiles = [ - "my/test/file1.js", - "my/test/file2.js", - "my/test/file3.js", - "my/test/nested/file1.js", - "my/test/nested/file2.js", - "my/test/nested/file3.js" + path.join("my", "test", "file1.js"), + path.join("my", "test", "file2.js"), + path.join("my", "test", "file3.js"), + path.join("my", "test", "nested", "file1.js"), + path.join("my", "test", "nested", "file2.js"), + path.join("my", "test", "nested", "file3.js") ]; const syncFilesMockData = { projectDir: projectDirPath, diff --git a/test/stubs.ts b/test/stubs.ts index 171c04846e..a3a303c73d 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -859,6 +859,22 @@ export class MarkingModeServiceStub implements IMarkingModeService { } } +export class AnalyticsService implements IAnalyticsService { + async checkConsent(): Promise { return ; } + async trackFeature(featureName: string): Promise { return ; } + async trackException(exception: any, message: string): Promise { return ; } + async setStatus(settingName: string, enabled: boolean): Promise { return ; } + async getStatusMessage(settingName: string, jsonFormat: boolean, readableSettingName: string): Promise { return "Fake message"; } + async isEnabled(settingName: string): Promise { return false; } + async track(featureName: string, featureValue: string): Promise { return ; } + async trackEventActionInGoogleAnalytics(data: IEventActionData) { return Promise.resolve(); } + async trackInGoogleAnalytics(data: IGoogleAnalyticsData) { return Promise.resolve(); } + async trackAcceptFeatureUsage(settings: { acceptTrackFeatureUsage: boolean }) { return Promise.resolve(); } + async trackPreviewAppData() { return Promise.resolve(); } + async finishTracking() { return Promise.resolve(); } + setShouldDispose() {} +} + export class InjectorStub extends Yok implements IInjector { constructor() { super();