diff --git a/PublicAPI.md b/PublicAPI.md index 10b6a2f488..9e42137ba9 100644 --- a/PublicAPI.md +++ b/PublicAPI.md @@ -898,6 +898,29 @@ getUserAgentString(identifier: string): string; const userAgentString = tns.analyticsSettingsService.getUserAgentString("tns/3.3.0"); ``` +### getPlaygroundInfo +The `getPlaygroundInfo` method allows retrieving information for projects that are exported from playground + +* Definition: +```TypeScript +/** + * Gets information for projects that are exported from playground. + * Returns null in case when project does not have playground key in package.json file (e.g is not exported from playground) and no playground info is saved in userSettings file + * @param {string} projectDir The project directory. + * @returns {Promise} Playground info. { id: string, usedTutorial: boolean } + */ +getPlaygroundInfo(projectDir: string): Promise; +``` + +* Usage: +```JavaScript +tns.analyticsSettingsService.getPlaygroundInfo("/my/project/path") + .then(playgroundInfo => { + console.log(playgroundInfo.id); + console.log(playgroundInfo.usedTutorial); + }); +``` + ## How to add a new method to Public API CLI is designed as command line tool and when it is used as a library, it does not give you access to all of the methods. This is mainly implementation detail. Most of the CLI's code is created to work in command line, not as a library, so before adding method to public API, most probably it will require some modification. For example the `$options` injected module contains information about all `--` options passed on the terminal. When the CLI is used as a library, the options are not populated. Before adding method to public API, make sure its implementation does not rely on `$options`. diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 08dcd977b3..12106bcd8c 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -144,3 +144,5 @@ $injector.requirePublic("extensibilityService", "./services/extensibility-servic $injector.require("nodeModulesDependenciesBuilder", "./tools/node-modules/node-modules-dependencies-builder"); $injector.require("subscriptionService", "./services/subscription-service"); + +$injector.require('playgroundService', './services/playground-service'); diff --git a/lib/common b/lib/common index 623c2d8afc..8bba82ae34 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 623c2d8afc376f726f1c2ecb784eef667398a395 +Subproject commit 8bba82ae3457b45775d244e498f6e3b9e8236f6d diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index c4d3607321..5044c0879d 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -105,7 +105,7 @@ interface IProjectDataService { */ removeDependency(projectDir: string, dependencyName: string): void; - getProjectData(projectDir: string): IProjectData; + getProjectData(projectDir?: string): IProjectData; } /** diff --git a/lib/services/analytics-settings-service.ts b/lib/services/analytics-settings-service.ts index 8930796165..4a0a85d4e2 100644 --- a/lib/services/analytics-settings-service.ts +++ b/lib/services/analytics-settings-service.ts @@ -8,7 +8,8 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService { private $staticConfig: IStaticConfig, private $hostInfo: IHostInfo, private $osInfo: IOsInfo, - private $logger: ILogger) { } + private $logger: ILogger, + private $playgroundService: IPlaygroundService) { } public async canDoRequest(): Promise { return true; @@ -23,6 +24,11 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService { return this.getSettingValueOrDefault(this.$staticConfig.ANALYTICS_INSTALLATION_ID_SETTING_NAME); } + @exported("analyticsSettingsService") + public async getPlaygroundInfo(projectDir: string): Promise { + return this.$playgroundService.getPlaygroundInfo(projectDir); + } + public getClientName(): string { return "" + this.$staticConfig.CLIENT_NAME_ALIAS.cyan.bold; } diff --git a/lib/services/analytics/google-analytics-custom-dimensions.d.ts b/lib/services/analytics/google-analytics-custom-dimensions.d.ts deleted file mode 100644 index 488439b814..0000000000 --- a/lib/services/analytics/google-analytics-custom-dimensions.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare 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 index 347625b9e8..2ce3a8284e 100644 --- a/lib/services/analytics/google-analytics-provider.ts +++ b/lib/services/analytics/google-analytics-provider.ts @@ -46,7 +46,7 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { this.setCrossClientCustomDimensions(visitor, sessionId); break; default: - this.setCustomDimensions(visitor, trackInfo.customDimensions, sessionId); + await this.setCustomDimensions(visitor, trackInfo.customDimensions, sessionId); break; } @@ -60,7 +60,7 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { } } - private setCustomDimensions(visitor: ua.Visitor, customDimensions: IStringDictionary, sessionId: string): void { + private async setCustomDimensions(visitor: ua.Visitor, customDimensions: IStringDictionary, sessionId: string): Promise { const defaultValues: IStringDictionary = { [GoogleAnalyticsCustomDimensions.cliVersion]: this.$staticConfig.version, [GoogleAnalyticsCustomDimensions.nodeVersion]: process.version, @@ -70,6 +70,12 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider { [GoogleAnalyticsCustomDimensions.client]: AnalyticsClients.Unknown }; + const playgrounInfo = await this.$analyticsSettingsService.getPlaygroundInfo(); + if (playgrounInfo && playgrounInfo.id) { + defaultValues[GoogleAnalyticsCustomDimensions.playgroundId] = playgrounInfo.id; + defaultValues[GoogleAnalyticsCustomDimensions.usedTutorial] = playgrounInfo.usedTutorial.toString(); + } + customDimensions = _.merge(defaultValues, customDimensions); _.each(customDimensions, (value, key) => { diff --git a/lib/services/playground-service.ts b/lib/services/playground-service.ts new file mode 100644 index 0000000000..a84fb17759 --- /dev/null +++ b/lib/services/playground-service.ts @@ -0,0 +1,49 @@ +export class PlaygroundService implements IPlaygroundService { + constructor(private $fs: IFileSystem, + private $projectDataService: IProjectDataService, + private $userSettingsService: IUserSettingsService) { } + + public async getPlaygroundInfo(projectDir?: string): Promise { + const projectData = this.getProjectData(projectDir); + if (projectData) { + const projectFileContent = this.$fs.readJson(projectData.projectFilePath); + if (this.hasPlaygroundKey(projectFileContent)) { + const id = projectFileContent.nativescript.playground.id; + let usedTutorial = projectFileContent.nativescript.playground.usedTutorial || false; + + // In case when usedTutorial=true is already saved in userSettings file, we shouldn't overwrite it + const playgroundInfo = await this.getPlaygroundInfoFromUserSettingsFile(); + if (playgroundInfo && playgroundInfo.usedTutorial) { + usedTutorial = true; + } + + delete projectFileContent.nativescript.playground; + this.$fs.writeJson(projectData.projectFilePath, projectFileContent); + + const result = { id , usedTutorial }; + await this.$userSettingsService.saveSettings({playground: result}); + return result; + } + } + + return this.getPlaygroundInfoFromUserSettingsFile(); + } + + private getProjectData(projectDir: string): IProjectData { + try { + return this.$projectDataService.getProjectData(projectDir); + } catch (e) { + // in case command is executed in non-project folder + return null; + } + } + + private hasPlaygroundKey(projectFileContent: any): boolean { + return projectFileContent && projectFileContent.nativescript && projectFileContent.nativescript.playground && projectFileContent.nativescript.playground.id; + } + + private async getPlaygroundInfoFromUserSettingsFile(): Promise { + return this.$userSettingsService.getSettingValue("playground"); + } +} +$injector.register('playgroundService', PlaygroundService); diff --git a/test/platform-commands.ts b/test/platform-commands.ts index 1ab1d431af..779e49f021 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -160,6 +160,9 @@ function createTestInjector() { message: (): void => undefined }) }); + testInjector.register("analyticsSettingsService", { + getPlaygroundInfo: () => Promise.resolve(null) + }); return testInjector; } diff --git a/test/plugins-service.ts b/test/plugins-service.ts index 1cdfbb5f67..ba10fd20ca 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -110,6 +110,9 @@ function createTestInjector() { message: (): void => undefined }) }); + testInjector.register("analyticsSettingsService", { + getPlaygroundInfo: () => Promise.resolve(null) + }); return testInjector; } diff --git a/test/services/playground-service.ts b/test/services/playground-service.ts new file mode 100644 index 0000000000..b160d25df1 --- /dev/null +++ b/test/services/playground-service.ts @@ -0,0 +1,212 @@ +import { assert } from "chai"; +import { FileSystemStub } from "../stubs"; +import { PlaygroundService } from "../../lib/services/playground-service"; +import { Yok } from "../../lib/common/yok"; + +let userSettings: any = null; + +function createTestInjector(): IInjector { + const testInjector = new Yok(); + + testInjector.register("playgroundService", PlaygroundService); + testInjector.register("fs", FileSystemStub); + testInjector.register("projectDataService", {}); + testInjector.register("userSettingsService", {}); + testInjector.register("injector", testInjector); + + return testInjector; +} + +function mockPlaygroundService(testInjector: IInjector, data?: { projectData?: any, nativescriptKey?: any, userSettingsData?: any}) { + const projectDataService = testInjector.resolve("projectDataService"); + projectDataService.getProjectData = () => (data && data.projectData) || {}; + + const userSettingsService = testInjector.resolve("userSettingsService"); + userSettingsService.getSettingValue = async (keyName: string) => { + return data && data.userSettingsData ? data.userSettingsData[keyName] : null; + }; + userSettingsService.saveSettings = async (settings: any) => { userSettings = settings; }; + + const fs = testInjector.resolve("fs"); + fs.readJson = () => (data && data.nativescriptKey) || {}; +} + +describe("PlaygroundService", () => { + let testInjector: IInjector = null; + let playgroundService: IPlaygroundService = null; + + beforeEach(() => { + testInjector = createTestInjector(); + playgroundService = testInjector.resolve("playgroundService"); + }); + + describe("getPlaygroundInfo", () => { + it("should return null when projectDir is not specified and no playground data is saved in userSettings file", async () => { + mockPlaygroundService(testInjector, { userSettingsData: null }); + const result = await playgroundService.getPlaygroundInfo(); + assert.equal(result, null); + }); + it("should return saved playgroundData from userSettings file when projectDir is not specified and playground data is already saved in userSettings file", async () => { + mockPlaygroundService(testInjector, { userSettingsData: {playground: {id: "test-playground-identifier", usedTutorial: false}}}); + const actualResult = await playgroundService.getPlaygroundInfo(); + const expectedResult = {id: "test-playground-identifier", usedTutorial: false}; + assert.deepEqual(actualResult, expectedResult); + }); + it("should return null when projectFile has no nativescript key in package.json file and no playground data is saved in userSettings file", async () => { + mockPlaygroundService(testInjector, { userSettingsData: null }); + const result = await playgroundService.getPlaygroundInfo(); + assert.equal(result, null); + }); + it("should return saved playgroundData from userSettings file when projectFile has no nativescript key in package.json and some playground data is already saved in userSettings file", async () => { + mockPlaygroundService(testInjector, { userSettingsData: {playground: {id: "test-playground-identifier", usedTutorial: true}}}); + const actualResult = await playgroundService.getPlaygroundInfo(); + const expectedResult = {id: "test-playground-identifier", usedTutorial: true}; + assert.deepEqual(actualResult, expectedResult); + }); + + describe("should return playgroundInfo when project has playground key in package.json", () => { + it("and no usedTutorial", async () => { + const nativescriptKey = { + nativescript: { + playground: { + id: "test-guid" + } + } + }; + mockPlaygroundService(testInjector, { nativescriptKey }); + const actualResult = await playgroundService.getPlaygroundInfo(); + const expectedResult = { id: "test-guid", usedTutorial: false }; + assert.deepEqual(actualResult, expectedResult); + }); + it("and usedTutorial is true", async() => { + const nativescriptKey = { + nativescript: { + playground: { + id: "test-guid", + usedTutorial: true + } + } + }; + mockPlaygroundService(testInjector, { nativescriptKey }); + const actualResult = await playgroundService.getPlaygroundInfo(); + const expectedResult = { id: 'test-guid', usedTutorial: true }; + assert.deepEqual(actualResult, expectedResult); + }); + it("and usedTutorial is false", async () => { + const nativescriptKey = { + nativescript: { + playground: { + id: "playground-test-guid", + usedTutorial: false + } + } + }; + mockPlaygroundService(testInjector, { nativescriptKey }); + const actualResult = await playgroundService.getPlaygroundInfo(); + const expectedResult = { id: "playground-test-guid", usedTutorial: false }; + assert.deepEqual(actualResult, expectedResult); + }); + }); + + describe("should return playgroundInfo from userSettings file", () => { + it("when usedTutorial is true", async () => { + mockPlaygroundService(testInjector, { userSettingsData: {playground: {id: "test-playground-identifier", usedTutorial: true}}}); + const actualResult = await playgroundService.getPlaygroundInfo(); + const expectedResult = { id: 'test-playground-identifier', usedTutorial: true }; + assert.deepEqual(actualResult, expectedResult); + }); + it("when usedTutorial is false", async () => { + mockPlaygroundService(testInjector, { userSettingsData: {playground: {id: "test-playground-identifier", usedTutorial: false}}}); + const actualResult = await playgroundService.getPlaygroundInfo(); + const expectedResult = { id: 'test-playground-identifier', usedTutorial: false }; + assert.deepEqual(actualResult, expectedResult); + }); + }); + + it("should return undefined when userSettings file does not have playground key", async () => { + mockPlaygroundService(testInjector, { userSettingsData: {}}); + const actualResult = await playgroundService.getPlaygroundInfo(); + assert.deepEqual(actualResult, undefined); + }); + it("should replace playgroundId when another id is already saved in userSettings file", async () => { + const nativescriptKey = { + nativescript: { + playground: { + id: "test-guid" + } + } + }; + mockPlaygroundService(testInjector, { nativescriptKey }); + let actualResult = await playgroundService.getPlaygroundInfo(); + let expectedResult = { id: "test-guid", usedTutorial: false }; + assert.deepEqual(actualResult, expectedResult); + + const secondNativescriptKey = { + nativescript: { + playground: { + id: "another-test-guid" + } + } + }; + mockPlaygroundService(testInjector, { nativescriptKey: secondNativescriptKey }); + actualResult = await playgroundService.getPlaygroundInfo(); + expectedResult = { id: 'another-test-guid', usedTutorial: false }; + assert.deepEqual(actualResult, expectedResult); + assert.deepEqual(userSettings, { playground: { id: 'another-test-guid', usedTutorial: false }}); + }); + it("should replace usedTutorial when false value is already saved in userSettings file", async () => { + const nativescriptKey = { + nativescript: { + playground: { + id: "test-guid", + usedTutorial: false + } + } + }; + mockPlaygroundService(testInjector, { nativescriptKey }); + let actualResult = await playgroundService.getPlaygroundInfo(); + let expectedResult = { id: "test-guid", usedTutorial: false }; + assert.deepEqual(actualResult, expectedResult); + + const secondNativescriptKey = { + nativescript: { + playground: { + id: "another-test-guid", + usedTutorial: true + } + } + }; + mockPlaygroundService(testInjector, { nativescriptKey: secondNativescriptKey, userSettingsData: {playground: { id: "test-guid", usedTutorial: false }} }); + actualResult = await playgroundService.getPlaygroundInfo(); + expectedResult = { id: 'another-test-guid', usedTutorial: true }; + assert.deepEqual(actualResult, expectedResult); + }); + it("shouldn't replace usedTutorial when true value is already saved in userSettings file", async () => { + const nativescriptKey = { + nativescript: { + playground: { + id: "test-guid", + usedTutorial: true + } + } + }; + mockPlaygroundService(testInjector, { nativescriptKey }); + let actualResult = await playgroundService.getPlaygroundInfo(); + let expectedResult = { id: "test-guid", usedTutorial: true }; + assert.deepEqual(actualResult, expectedResult); + + const secondNativescriptKey = { + nativescript: { + playground: { + id: "another-test-guid", + usedTutorial: false + } + } + }; + mockPlaygroundService(testInjector, { nativescriptKey: secondNativescriptKey, userSettingsData: {playground: { id: "test-guid", usedTutorial: true }} }); + actualResult = await playgroundService.getPlaygroundInfo(); + expectedResult = { id: 'another-test-guid', usedTutorial: true }; + assert.deepEqual(actualResult, expectedResult); + }); + }); +});