Skip to content

Commit 7150feb

Browse files
committed
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.
1 parent 4b5c98c commit 7150feb

11 files changed

+308
-13
lines changed

PublicAPI.md

+22
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,28 @@ getUserAgentString(identifier: string): string;
898898
const userAgentString = tns.analyticsSettingsService.getUserAgentString("tns/3.3.0");
899899
```
900900
901+
### getPlaygroundInfo
902+
The `getPlaygroundInfo` method allows retrieving information for projects that are exported from playground
903+
904+
* Definition:
905+
```TypeScript
906+
/**
907+
* Gets information for projects that are exported from playground
908+
* @param {string} projectDir The project directory.
909+
* @returns {Promise<IPlaygroundInfo>} Playground info. { id: string, usedTutorial: boolean }
910+
*/
911+
getPlaygroundInfo(projectDir: string): Promise<IPlaygroundInfo>;
912+
```
913+
914+
* Usage:
915+
```JavaScript
916+
tns.analyticsSettingsService.getPlaygroundInfo("/my/project/path")
917+
.then(playgroundInfo => {
918+
console.log(playgroundInfo.id);
919+
console.log(playgroundInfo.usedTutorial);
920+
});
921+
```
922+
901923
## How to add a new method to Public API
902924
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.
903925
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`.

lib/bootstrap.ts

+2
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,5 @@ $injector.requirePublic("extensibilityService", "./services/extensibility-servic
144144

145145
$injector.require("nodeModulesDependenciesBuilder", "./tools/node-modules/node-modules-dependencies-builder");
146146
$injector.require("subscriptionService", "./services/subscription-service");
147+
148+
$injector.require('playgroundService', './services/playground-service');

lib/definitions/project.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ interface IProjectDataService {
105105
*/
106106
removeDependency(projectDir: string, dependencyName: string): void;
107107

108-
getProjectData(projectDir: string): IProjectData;
108+
getProjectData(projectDir?: string): IProjectData;
109109
}
110110

111111
/**

lib/services/analytics-settings-service.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService {
88
private $staticConfig: IStaticConfig,
99
private $hostInfo: IHostInfo,
1010
private $osInfo: IOsInfo,
11-
private $logger: ILogger) { }
11+
private $logger: ILogger,
12+
private $playgroundService: IPlaygroundService) { }
1213

1314
public async canDoRequest(): Promise<boolean> {
1415
return true;
@@ -23,6 +24,11 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService {
2324
return this.getSettingValueOrDefault(this.$staticConfig.ANALYTICS_INSTALLATION_ID_SETTING_NAME);
2425
}
2526

27+
@exported("analyticsSettingsService")
28+
public async getPlaygroundInfo(projectDir: string): Promise<IPlaygroundInfo> {
29+
return this.$playgroundService.getPlaygroundInfo(projectDir);
30+
}
31+
2632
public getClientName(): string {
2733
return "" + this.$staticConfig.CLIENT_NAME_ALIAS.cyan.bold;
2834
}

lib/services/analytics/google-analytics-custom-dimensions.d.ts

-8
This file was deleted.

lib/services/analytics/google-analytics-provider.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider {
1111
private $staticConfig: IStaticConfig,
1212
private $analyticsSettingsService: IAnalyticsSettingsService,
1313
private $logger: ILogger,
14+
private $playgroundService: IPlaygroundService,
1415
private $proxyService: IProxyService) {
1516
}
1617

@@ -46,7 +47,7 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider {
4647
this.setCrossClientCustomDimensions(visitor, sessionId);
4748
break;
4849
default:
49-
this.setCustomDimensions(visitor, trackInfo.customDimensions, sessionId);
50+
await this.setCustomDimensions(visitor, trackInfo.customDimensions, sessionId);
5051
break;
5152
}
5253

@@ -60,7 +61,7 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider {
6061
}
6162
}
6263

63-
private setCustomDimensions(visitor: ua.Visitor, customDimensions: IStringDictionary, sessionId: string): void {
64+
private async setCustomDimensions(visitor: ua.Visitor, customDimensions: IStringDictionary, sessionId: string): Promise<void> {
6465
const defaultValues: IStringDictionary = {
6566
[GoogleAnalyticsCustomDimensions.cliVersion]: this.$staticConfig.version,
6667
[GoogleAnalyticsCustomDimensions.nodeVersion]: process.version,
@@ -70,6 +71,12 @@ export class GoogleAnalyticsProvider implements IGoogleAnalyticsProvider {
7071
[GoogleAnalyticsCustomDimensions.client]: AnalyticsClients.Unknown
7172
};
7273

74+
const playgrounInfo = await this.$playgroundService.getPlaygroundInfo();
75+
if (playgrounInfo && playgrounInfo.id) {
76+
defaultValues[GoogleAnalyticsCustomDimensions.playgroundId] = playgrounInfo.id;
77+
defaultValues[GoogleAnalyticsCustomDimensions.usedTutorial] = playgrounInfo.usedTutorial.toString();
78+
}
79+
7380
customDimensions = _.merge(defaultValues, customDimensions);
7481

7582
_.each(customDimensions, (value, key) => {

lib/services/playground-service.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export class PlaygroundService implements IPlaygroundService {
2+
constructor(private $fs: IFileSystem,
3+
private $projectDataService: IProjectDataService,
4+
private $userSettingsService: IUserSettingsService) { }
5+
6+
public async getPlaygroundInfo(projectDir?: string): Promise<IPlaygroundInfo> {
7+
const projectData = this.getProjectData(projectDir);
8+
if (projectData) {
9+
const projectFileContent = this.$fs.readJson(projectData.projectFilePath);
10+
if (this.hasPlaygroundKey(projectFileContent)) {
11+
const id = projectFileContent.nativescript.playground.id;
12+
let usedTutorial = projectFileContent.nativescript.playground.usedTutorial || false;
13+
14+
// In case when usedTutorial=true is already saved in userSettings file, we shouldn't overwrite it
15+
const playgroundInfo = await this.getPlaygroundInfoFromUserSettingsFile();
16+
if (playgroundInfo && playgroundInfo.usedTutorial) {
17+
usedTutorial = true;
18+
}
19+
20+
delete projectFileContent.nativescript.playground;
21+
this.$fs.writeJson(projectData.projectFilePath, projectFileContent);
22+
23+
const result = { id , usedTutorial };
24+
await this.$userSettingsService.saveSettings(<any>{playground: result});
25+
return result;
26+
}
27+
}
28+
29+
return this.getPlaygroundInfoFromUserSettingsFile();
30+
}
31+
32+
private getProjectData(projectDir: string): IProjectData {
33+
try {
34+
return this.$projectDataService.getProjectData(projectDir);
35+
} catch (e) { // in case command is executed in non-project folder
36+
return null;
37+
}
38+
}
39+
40+
private hasPlaygroundKey(projectFileContent: any): boolean {
41+
return projectFileContent && projectFileContent.nativescript && projectFileContent.nativescript.playground && projectFileContent.nativescript.playground.id;
42+
}
43+
44+
private async getPlaygroundInfoFromUserSettingsFile(): Promise<IPlaygroundInfo> {
45+
return this.$userSettingsService.getSettingValue<IPlaygroundInfo>("playground");
46+
}
47+
}
48+
$injector.register('playgroundService', PlaygroundService);

test/platform-commands.ts

+3
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ function createTestInjector() {
160160
message: (): void => undefined
161161
})
162162
});
163+
testInjector.register("playgroundService", {
164+
getPlaygroundInfo: () => Promise.resolve(null)
165+
});
163166

164167
return testInjector;
165168
}

test/plugins-service.ts

+3
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ function createTestInjector() {
110110
message: (): void => undefined
111111
})
112112
});
113+
testInjector.register("playgroundService", {
114+
getPlaygroundInfo: () => Promise.resolve(null)
115+
});
113116

114117
return testInjector;
115118
}

test/services/playground-service.ts

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import { assert } from "chai";
2+
import { FileSystemStub } from "../stubs";
3+
import { PlaygroundService } from "../../lib/services/playground-service";
4+
import { Yok } from "../../lib/common/yok";
5+
6+
let userSettings: any = null;
7+
8+
function createTestInjector(): IInjector {
9+
const testInjector = new Yok();
10+
11+
testInjector.register("playgroundService", PlaygroundService);
12+
testInjector.register("fs", FileSystemStub);
13+
testInjector.register("projectDataService", {});
14+
testInjector.register("userSettingsService", {});
15+
testInjector.register("injector", testInjector);
16+
17+
return testInjector;
18+
}
19+
20+
function mockPlaygroundService(testInjector: IInjector, data?: { projectData?: any, nativescriptKey?: any, userSettingsData?: any}) {
21+
const projectDataService = testInjector.resolve<IProjectDataService>("projectDataService");
22+
projectDataService.getProjectData = () => (data && data.projectData) || <IProjectData>{};
23+
24+
const userSettingsService = testInjector.resolve("userSettingsService");
25+
userSettingsService.getSettingValue = async (keyName: string) => {
26+
return data && data.userSettingsData ? data.userSettingsData[keyName] : null;
27+
};
28+
userSettingsService.saveSettings = async (settings: any) => { userSettings = settings; };
29+
30+
const fs = testInjector.resolve<IFileSystem>("fs");
31+
fs.readJson = () => (data && data.nativescriptKey) || {};
32+
}
33+
34+
describe.only("PlaygroundService", () => {
35+
let testInjector: IInjector = null;
36+
let playgroundService: IPlaygroundService = null;
37+
38+
beforeEach(() => {
39+
testInjector = createTestInjector();
40+
playgroundService = testInjector.resolve("playgroundService");
41+
});
42+
43+
describe("getPlaygroundInfo", () => {
44+
it("should return null when projectDir is not specified no playground data is saved in userSettings file", async () => {
45+
mockPlaygroundService(testInjector, { userSettingsData: null });
46+
const result = await playgroundService.getPlaygroundInfo();
47+
assert.equal(result, null);
48+
});
49+
it("should return saved playgroundData from userSettings file when projectDir is not specified and some playground data is already saved in userSettings file", async () => {
50+
mockPlaygroundService(testInjector, { userSettingsData: {playground: {id: "test-playground-identifier", usedTutorial: false}}});
51+
const actualResult = await playgroundService.getPlaygroundInfo();
52+
const expectedResult = {id: "test-playground-identifier", usedTutorial: false};
53+
assert.deepEqual(actualResult, expectedResult);
54+
});
55+
it("should return null when projectFile has no nativescript key in package.json file and no playground data is saved in userSettings file", async () => {
56+
mockPlaygroundService(testInjector, { userSettingsData: null });
57+
const result = await playgroundService.getPlaygroundInfo();
58+
assert.equal(result, null);
59+
});
60+
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 () => {
61+
mockPlaygroundService(testInjector, { userSettingsData: {playground: {id: "test-playground-identifier", usedTutorial: true}}});
62+
const actualResult = await playgroundService.getPlaygroundInfo();
63+
const expectedResult = {id: "test-playground-identifier", usedTutorial: true};
64+
assert.deepEqual(actualResult, expectedResult);
65+
});
66+
67+
describe("should return playgroundInfo when project has playground key in package.json", () => {
68+
it("and no usedTutorial", async () => {
69+
const nativescriptKey = {
70+
nativescript: {
71+
playground: {
72+
id: "test-guid"
73+
}
74+
}
75+
};
76+
mockPlaygroundService(testInjector, { nativescriptKey });
77+
const actualResult = await playgroundService.getPlaygroundInfo();
78+
const expectedResult = { id: "test-guid", usedTutorial: false };
79+
assert.deepEqual(actualResult, expectedResult);
80+
});
81+
it("and usedTutorial is true", async() => {
82+
const nativescriptKey = {
83+
nativescript: {
84+
playground: {
85+
id: "test-guid",
86+
usedTutorial: true
87+
}
88+
}
89+
};
90+
mockPlaygroundService(testInjector, { nativescriptKey });
91+
const actualResult = await playgroundService.getPlaygroundInfo();
92+
const expectedResult = { id: 'test-guid', usedTutorial: true };
93+
assert.deepEqual(actualResult, expectedResult);
94+
});
95+
it("and usedTutorial is false", async () => {
96+
const nativescriptKey = {
97+
nativescript: {
98+
playground: {
99+
id: "playground-test-guid",
100+
usedTutorial: false
101+
}
102+
}
103+
};
104+
mockPlaygroundService(testInjector, { nativescriptKey });
105+
const actualResult = await playgroundService.getPlaygroundInfo();
106+
const expectedResult = { id: "playground-test-guid", usedTutorial: false };
107+
assert.deepEqual(actualResult, expectedResult);
108+
});
109+
});
110+
111+
describe("should return playgroundInfo from userSettings file", () => {
112+
it("when usedTutorial is true", async () => {
113+
mockPlaygroundService(testInjector, { userSettingsData: {playground: {id: "test-playground-identifier", usedTutorial: true}}});
114+
const actualResult = await playgroundService.getPlaygroundInfo();
115+
const expectedResult = { id: 'test-playground-identifier', usedTutorial: true };
116+
assert.deepEqual(actualResult, expectedResult);
117+
});
118+
it("when usedTutorial is false", async () => {
119+
mockPlaygroundService(testInjector, { userSettingsData: {playground: {id: "test-playground-identifier", usedTutorial: false}}});
120+
const actualResult = await playgroundService.getPlaygroundInfo();
121+
const expectedResult = { id: 'test-playground-identifier', usedTutorial: false };
122+
assert.deepEqual(actualResult, expectedResult);
123+
});
124+
});
125+
126+
it("should return undefined when userSettings file does not have playground key", async () => {
127+
mockPlaygroundService(testInjector, { userSettingsData: {}});
128+
const actualResult = await playgroundService.getPlaygroundInfo();
129+
assert.deepEqual(actualResult, undefined);
130+
});
131+
it("should replace playgroundId when another id is already saved in userSettings file", async () => {
132+
const nativescriptKey = {
133+
nativescript: {
134+
playground: {
135+
id: "test-guid"
136+
}
137+
}
138+
};
139+
mockPlaygroundService(testInjector, { nativescriptKey });
140+
let actualResult = await playgroundService.getPlaygroundInfo();
141+
let expectedResult = { id: "test-guid", usedTutorial: false };
142+
assert.deepEqual(actualResult, expectedResult);
143+
144+
const secondNativescriptKey = {
145+
nativescript: {
146+
playground: {
147+
id: "another-test-guid"
148+
}
149+
}
150+
};
151+
mockPlaygroundService(testInjector, { nativescriptKey: secondNativescriptKey });
152+
actualResult = await playgroundService.getPlaygroundInfo();
153+
expectedResult = { id: 'another-test-guid', usedTutorial: false };
154+
assert.deepEqual(actualResult, expectedResult);
155+
assert.deepEqual(userSettings, { playground: { id: 'another-test-guid', usedTutorial: false }});
156+
});
157+
it("should replace usedTutorial when false value is already saved in userSettings file", async () => {
158+
const nativescriptKey = {
159+
nativescript: {
160+
playground: {
161+
id: "test-guid",
162+
usedTutorial: false
163+
}
164+
}
165+
};
166+
mockPlaygroundService(testInjector, { nativescriptKey });
167+
let actualResult = await playgroundService.getPlaygroundInfo();
168+
let expectedResult = { id: "test-guid", usedTutorial: false };
169+
assert.deepEqual(actualResult, expectedResult);
170+
171+
const secondNativescriptKey = {
172+
nativescript: {
173+
playground: {
174+
id: "another-test-guid",
175+
usedTutorial: true
176+
}
177+
}
178+
};
179+
mockPlaygroundService(testInjector, { nativescriptKey: secondNativescriptKey, userSettingsData: {playground: { id: "test-guid", usedTutorial: false }} });
180+
actualResult = await playgroundService.getPlaygroundInfo();
181+
expectedResult = { id: 'another-test-guid', usedTutorial: true };
182+
assert.deepEqual(actualResult, expectedResult);
183+
});
184+
it("shouldn't replace usedTutorial when true value is already saved in userSettings file", async () => {
185+
const nativescriptKey = {
186+
nativescript: {
187+
playground: {
188+
id: "test-guid",
189+
usedTutorial: true
190+
}
191+
}
192+
};
193+
mockPlaygroundService(testInjector, { nativescriptKey });
194+
let actualResult = await playgroundService.getPlaygroundInfo();
195+
let expectedResult = { id: "test-guid", usedTutorial: true };
196+
assert.deepEqual(actualResult, expectedResult);
197+
198+
const secondNativescriptKey = {
199+
nativescript: {
200+
playground: {
201+
id: "another-test-guid",
202+
usedTutorial: false
203+
}
204+
}
205+
};
206+
mockPlaygroundService(testInjector, { nativescriptKey: secondNativescriptKey, userSettingsData: {playground: { id: "test-guid", usedTutorial: true }} });
207+
actualResult = await playgroundService.getPlaygroundInfo();
208+
expectedResult = { id: 'another-test-guid', usedTutorial: true };
209+
assert.deepEqual(actualResult, expectedResult);
210+
});
211+
});
212+
});

0 commit comments

Comments
 (0)