Skip to content

feat(Analytics): Respect playground key from package.json file #3396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions PublicAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note when this method returns null or undefined.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


* 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<IPlaygroundInfo>} Playground info. { id: string, usedTutorial: boolean }
*/
getPlaygroundInfo(projectDir: string): Promise<IPlaygroundInfo>;
```

* 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`.
Expand Down
2 changes: 2 additions & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
2 changes: 1 addition & 1 deletion lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ interface IProjectDataService {
*/
removeDependency(projectDir: string, dependencyName: string): void;

getProjectData(projectDir: string): IProjectData;
getProjectData(projectDir?: string): IProjectData;
}

/**
Expand Down
8 changes: 7 additions & 1 deletion lib/services/analytics-settings-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
return true;
Expand All @@ -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<IPlaygroundInfo> {
return this.$playgroundService.getPlaygroundInfo(projectDir);
}

public getClientName(): string {
return "" + this.$staticConfig.CLIENT_NAME_ALIAS.cyan.bold;
}
Expand Down

This file was deleted.

10 changes: 8 additions & 2 deletions lib/services/analytics/google-analytics-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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<void> {
const defaultValues: IStringDictionary = {
[GoogleAnalyticsCustomDimensions.cliVersion]: this.$staticConfig.version,
[GoogleAnalyticsCustomDimensions.nodeVersion]: process.version,
Expand All @@ -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) => {
Expand Down
49 changes: 49 additions & 0 deletions lib/services/playground-service.ts
Original file line number Diff line number Diff line change
@@ -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<IPlaygroundInfo> {
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(<any>{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<IPlaygroundInfo> {
return this.$userSettingsService.getSettingValue<IPlaygroundInfo>("playground");
}
}
$injector.register('playgroundService', PlaygroundService);
3 changes: 3 additions & 0 deletions test/platform-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ function createTestInjector() {
message: (): void => undefined
})
});
testInjector.register("analyticsSettingsService", {
getPlaygroundInfo: () => Promise.resolve(null)
});

return testInjector;
}
Expand Down
3 changes: 3 additions & 0 deletions test/plugins-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ function createTestInjector() {
message: (): void => undefined
})
});
testInjector.register("analyticsSettingsService", {
getPlaygroundInfo: () => Promise.resolve(null)
});

return testInjector;
}
Expand Down
212 changes: 212 additions & 0 deletions test/services/playground-service.ts
Original file line number Diff line number Diff line change
@@ -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<IProjectDataService>("projectDataService");
projectDataService.getProjectData = () => (data && data.projectData) || <IProjectData>{};

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<IFileSystem>("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);
});
});
});