From 76e29f81adab4c32bca7463bdecc789bcc8c5e76 Mon Sep 17 00:00:00 2001 From: fatme Date: Tue, 27 Feb 2018 14:06:12 +0200 Subject: [PATCH] feat(Analytics): Respect playground key from package.json file We need to track users that export their projects from playground and opens them in CLI or Sidekick. To support that we introduce playground key in nativescript key in package.json file. For example: ``` { "nativescript": { "playground": { "id": "some user quid", "usedTutorial": false // is not obligatory. In case it is present, can be true or false } } } ``` If case when package.json file contains playground key, {N} CLI reads playground data and saves them in userSettings file. If usedTutorial=true is already saved in userSettings file, {N} CLI does not overwrite it. After that {N} CLI deletes playground key from package.json file. In case when package.json file does not contain playground key, {N} CLI checks if playground key is already saved in userSettings file. It this is the case, {N} CLI reads playground data from userSettings file. --- PublicAPI.md | 23 ++ lib/bootstrap.ts | 2 + lib/common | 2 +- lib/definitions/project.d.ts | 2 +- lib/services/analytics-settings-service.ts | 8 +- .../google-analytics-custom-dimensions.d.ts | 8 - .../analytics/google-analytics-provider.ts | 10 +- lib/services/playground-service.ts | 49 ++++ test/platform-commands.ts | 3 + test/plugins-service.ts | 3 + test/services/playground-service.ts | 212 ++++++++++++++++++ 11 files changed, 309 insertions(+), 13 deletions(-) delete mode 100644 lib/services/analytics/google-analytics-custom-dimensions.d.ts create mode 100644 lib/services/playground-service.ts create mode 100644 test/services/playground-service.ts 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); + }); + }); +});