Skip to content

Commit 2d76ca5

Browse files
Add tracking for the project types that users are creating and working with (#2497)
* Track from which template a project is created Add tracking from which template a project is created. This will give us better picture of the usage of CLI and the types of projects that the users are creating. * Track project type when deploy/run/livesync is executed Track the project type (Angular, Pure TypeScript, Pure JavaScript) when any of the commands is executed: * prepare * deploy * run * livesync This will allow us to better understand the type of projects that the users are building.
1 parent 1abcec7 commit 2d76ca5

13 files changed

+200
-20
lines changed

lib/definitions/platform.d.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ interface IPlatformService {
142142
* @returns {string} The contents of the file or null when there is no such file.
143143
*/
144144
readFile(device: Mobile.IDevice, deviceFilePath: string): Promise<string>;
145+
146+
/**
147+
* Sends information to analytics for current project type.
148+
* The information is sent once per process for each project.
149+
* In long living process, where the project may change, each of the projects will be tracked after it's being opened.
150+
* @returns {Promise<void>}
151+
*/
152+
trackProjectType(): Promise<void>;
145153
}
146154

147155
interface IPlatformData {
@@ -183,4 +191,4 @@ interface INodeModulesDependenciesBuilder {
183191
interface IBuildInfo {
184192
prepareTime: string;
185193
buildTime: string;
186-
}
194+
}

lib/definitions/project.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ interface IProjectData {
5353
projectFilePath: string;
5454
projectId?: string;
5555
dependencies: any;
56+
devDependencies: IStringDictionary;
5657
appDirectoryPath: string;
5758
appResourcesDirectoryPath: string;
59+
projectType: string;
5860
}
5961

6062
interface IProjectDataService {

lib/project-data.ts

+43
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,33 @@ import * as constants from "./constants";
22
import * as path from "path";
33
import { EOL } from "os";
44

5+
interface IProjectType {
6+
type: string;
7+
requiredDependencies?: string[];
8+
isDefaultProjectType?: boolean;
9+
}
10+
511
export class ProjectData implements IProjectData {
612
private static OLD_PROJECT_FILE_NAME = ".tnsproject";
713

14+
/**
15+
* NOTE: Order of the elements is important as the TypeScript dependencies are commonly included in Angular project as well.
16+
*/
17+
private static PROJECT_TYPES: IProjectType[] = [
18+
{
19+
type: "Pure JavaScript",
20+
isDefaultProjectType: true
21+
},
22+
{
23+
type: "Angular",
24+
requiredDependencies: ["@angular/core", "nativescript-angular"]
25+
},
26+
{
27+
type: "Pure TypeScript",
28+
requiredDependencies: ["typescript", "nativescript-dev-typescript"]
29+
}
30+
];
31+
832
public projectDir: string;
933
public platformsDir: string;
1034
public projectFilePath: string;
@@ -13,6 +37,8 @@ export class ProjectData implements IProjectData {
1337
public appDirectoryPath: string;
1438
public appResourcesDirectoryPath: string;
1539
public dependencies: any;
40+
public devDependencies: IStringDictionary;
41+
public projectType: string;
1642

1743
constructor(private $fs: IFileSystem,
1844
private $errors: IErrors,
@@ -48,6 +74,8 @@ export class ProjectData implements IProjectData {
4874
if (data) {
4975
this.projectId = data.id;
5076
this.dependencies = fileContent.dependencies;
77+
this.devDependencies = fileContent.devDependencies;
78+
this.projectType = this.getProjectType();
5179
} else { // This is the case when we have package.json file but nativescipt key is not presented in it
5280
this.tryToUpgradeProject();
5381
}
@@ -57,6 +85,21 @@ export class ProjectData implements IProjectData {
5785
}
5886
}
5987

88+
private getProjectType(): string {
89+
let detectedProjectType = _.find(ProjectData.PROJECT_TYPES, (projectType) => projectType.isDefaultProjectType).type;
90+
91+
const deps: string[] = _.keys(this.dependencies).concat(_.keys(this.devDependencies));
92+
93+
_.each(ProjectData.PROJECT_TYPES, projectType => {
94+
if (_.some(projectType.requiredDependencies, requiredDependency => deps.indexOf(requiredDependency) !== -1)) {
95+
detectedProjectType = projectType.type;
96+
return false;
97+
}
98+
});
99+
100+
return detectedProjectType;
101+
}
102+
60103
private throwNoProjectFoundError(): void {
61104
this.$errors.fail("No project found at or above '%s' and neither was a --path specified.", this.$options.path || path.resolve("."));
62105
}

lib/services/livesync/livesync-service.ts

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ class LiveSyncService implements ILiveSyncService {
102102

103103
@helpers.hook('livesync')
104104
private async liveSyncCore(liveSyncData: ILiveSyncData[], applicationReloadAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => Promise<void>): Promise<void> {
105+
await this.$platformService.trackProjectType();
106+
105107
let watchForChangeActions: ((event: string, filePath: string, dispatcher: IFutureDispatcher) => Promise<void>)[] = [];
106108

107109
for (let dataItem of liveSyncData) {

lib/services/platform-service.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export class PlatformService implements IPlatformService {
1717
return this.$hooksService;
1818
}
1919

20+
private _trackedProjectFilePath: string = null;
21+
2022
constructor(private $devicesService: Mobile.IDevicesService,
2123
private $errors: IErrors,
2224
private $fs: IFileSystem,
@@ -38,7 +40,8 @@ export class PlatformService implements IPlatformService {
3840
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
3941
private $deviceAppDataFactory: Mobile.IDeviceAppDataFactory,
4042
private $projectChangesService: IProjectChangesService,
41-
private $emulatorPlatformService: IEmulatorPlatformService) { }
43+
private $emulatorPlatformService: IEmulatorPlatformService,
44+
private $analyticsService: IAnalyticsService) { }
4245

4346
public async addPlatforms(platforms: string[]): Promise<void> {
4447
let platformsDir = this.$projectData.platformsDir;
@@ -186,6 +189,8 @@ export class PlatformService implements IPlatformService {
186189
public async preparePlatform(platform: string): Promise<boolean> {
187190
this.validatePlatform(platform);
188191

192+
await this.trackProjectType();
193+
189194
//We need dev-dependencies here, so before-prepare hooks will be executed correctly.
190195
try {
191196
await this.$pluginsService.ensureAllDependenciesAreInstalled();
@@ -314,7 +319,7 @@ export class PlatformService implements IPlatformService {
314319
}
315320

316321
public async shouldBuild(platform: string, buildConfig?: IBuildConfig): Promise<boolean> {
317-
if (this.$projectChangesService.currentChanges.changesRequireBuild) {
322+
if (this.$projectChangesService.currentChanges.changesRequireBuild) {
318323
return true;
319324
}
320325
let platformData = this.$platformsData.getPlatformData(platform);
@@ -342,8 +347,21 @@ export class PlatformService implements IPlatformService {
342347
return prepareInfo.changesRequireBuildTime !== buildInfo.prepareTime;
343348
}
344349

350+
public async trackProjectType(): Promise<void> {
351+
// Track each project once per process.
352+
// In long living process, where we may work with multiple projects, we would like to track the information for each of them.
353+
if (this.$projectData && (this.$projectData.projectFilePath !== this._trackedProjectFilePath)) {
354+
this._trackedProjectFilePath = this.$projectData.projectFilePath;
355+
356+
await this.$analyticsService.track("Working with project type", this.$projectData.projectType);
357+
}
358+
}
359+
345360
public async buildPlatform(platform: string, buildConfig?: IBuildConfig): Promise<void> {
346361
this.$logger.out("Building project...");
362+
363+
await this.trackProjectType();
364+
347365
let platformData = this.$platformsData.getPlatformData(platform);
348366
await platformData.platformProjectService.buildProject(platformData.projectRoot, buildConfig);
349367
let prepareInfo = this.$projectChangesService.getPrepareInfo(platform);
@@ -417,6 +435,8 @@ export class PlatformService implements IPlatformService {
417435
}
418436

419437
public async runPlatform(platform: string): Promise<void> {
438+
await this.trackProjectType();
439+
420440
if (this.$options.justlaunch) {
421441
this.$options.watch = false;
422442
}

lib/services/project-templates-service.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,24 @@ temp.track();
55

66
export class ProjectTemplatesService implements IProjectTemplatesService {
77

8-
public constructor(private $fs: IFileSystem,
8+
public constructor(private $analyticsService: IAnalyticsService,
9+
private $fs: IFileSystem,
910
private $logger: ILogger,
1011
private $npmInstallationManager: INpmInstallationManager) { }
1112

1213
public async prepareTemplate(originalTemplateName: string, projectDir: string): Promise<string> {
13-
let realTemplatePath: string;
1414
// support <reserved_name>@<version> syntax
1515
let data = originalTemplateName.split("@"),
1616
name = data[0],
1717
version = data[1];
1818

19-
if (constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()]) {
20-
realTemplatePath = await this.prepareNativeScriptTemplate(constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()], version, projectDir);
21-
} else {
22-
// Use the original template name, specified by user as it may be case-sensitive.
23-
realTemplatePath = await this.prepareNativeScriptTemplate(originalTemplateName, version, projectDir);
24-
}
19+
const templateName = constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()] || name;
2520

26-
//this removes dependencies from templates so they are not copied to app folder
21+
await this.$analyticsService.track("Template used for project creation", templateName);
22+
23+
const realTemplatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir);
24+
25+
// this removes dependencies from templates so they are not copied to app folder
2726
this.$fs.deleteDirectory(path.join(realTemplatePath, constants.NODE_MODULES_FOLDER_NAME));
2827

2928
return realTemplatePath;

test/npm-support.ts

+4
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ function createTestInjector(): IInjector {
7979
testInjector.register("config", StaticConfigLib.Configuration);
8080
testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService);
8181
testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService);
82+
testInjector.register("analyticsService", {
83+
track: async () => undefined
84+
});
85+
8286
return testInjector;
8387
}
8488

test/platform-commands.ts

+4
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ function createTestInjector() {
136136
testInjector.register("childProcess", ChildProcessLib.ChildProcess);
137137
testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService);
138138
testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService);
139+
testInjector.register("analyticsService", {
140+
track: async () => undefined
141+
});
142+
139143
return testInjector;
140144
}
141145

test/platform-service.ts

+3
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ function createTestInjector() {
7676
testInjector.register("childProcess", ChildProcessLib.ChildProcess);
7777
testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService);
7878
testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService);
79+
testInjector.register("analyticsService", {
80+
track: async () => undefined
81+
});
7982

8083
return testInjector;
8184
}

test/project-data.ts

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { ProjectData } from "../lib/project-data";
2+
import { Yok } from "../lib/common/yok";
3+
import { assert } from "chai";
4+
import * as stubs from "./stubs";
5+
import * as path from "path";
6+
7+
describe("projectData", () => {
8+
const createTestInjector = (): IInjector => {
9+
const testInjector = new Yok();
10+
11+
testInjector.register("projectHelper", {
12+
projectDir: null,
13+
sanitizeName: (name: string) => name
14+
});
15+
16+
testInjector.register("fs", {
17+
exists: () => true,
18+
readJson: (): any => null
19+
});
20+
21+
testInjector.register("staticConfig", {
22+
CLIENT_NAME_KEY_IN_PROJECT_FILE: "nativescript",
23+
PROJECT_FILE_NAME: "package.json"
24+
});
25+
26+
testInjector.register("errors", stubs.ErrorsStub);
27+
28+
testInjector.register("logger", stubs.LoggerStub);
29+
30+
testInjector.register("options", {});
31+
32+
testInjector.register("projectData", ProjectData);
33+
34+
return testInjector;
35+
};
36+
37+
describe("projectType", () => {
38+
39+
const assertProjectType = (dependencies: any, devDependencies: any, expectedProjecType: string) => {
40+
const testInjector = createTestInjector();
41+
const fs = testInjector.resolve("fs");
42+
fs.exists = (filePath: string) => filePath && path.basename(filePath) === "package.json";
43+
44+
fs.readJson = () => ({
45+
nativescript: {},
46+
dependencies: dependencies,
47+
devDependencies: devDependencies
48+
});
49+
50+
const projectHelper: IProjectHelper = testInjector.resolve("projectHelper");
51+
projectHelper.projectDir = "projectDir";
52+
53+
const projectData: IProjectData = testInjector.resolve("projectData");
54+
assert.deepEqual(projectData.projectType, expectedProjecType);
55+
};
56+
57+
it("detects project as Angular when @angular/core exists as dependency", () => {
58+
assertProjectType({ "@angular/core": "*" }, null, "Angular");
59+
});
60+
61+
it("detects project as Angular when nativescript-angular exists as dependency", () => {
62+
assertProjectType({ "nativescript-angular": "*" }, null, "Angular");
63+
});
64+
65+
it("detects project as TypeScript when nativescript-dev-typescript exists as dependency", () => {
66+
assertProjectType(null, { "nativescript-dev-typescript": "*" }, "Pure TypeScript");
67+
});
68+
69+
it("detects project as TypeScript when typescript exists as dependency", () => {
70+
assertProjectType(null, { "typescript": "*" }, "Pure TypeScript");
71+
});
72+
73+
it("detects project as JavaScript when no other project type is detected", () => {
74+
assertProjectType(null, null, "Pure JavaScript");
75+
});
76+
});
77+
});

test/project-service.ts

+18-8
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class ProjectIntegrationTest {
114114
this.testInjector.register("fs", FileSystem);
115115
this.testInjector.register("projectDataService", ProjectDataServiceLib.ProjectDataService);
116116
this.testInjector.register("staticConfig", StaticConfig);
117+
this.testInjector.register("analyticsService", { track: async () => undefined });
117118

118119
this.testInjector.register("npmInstallationManager", NpmInstallationManager);
119120
this.testInjector.register("npm", NpmLib.NodePackageManager);
@@ -153,8 +154,10 @@ describe("Project Service Tests", () => {
153154
"readme": "dummy",
154155
"repository": "dummy"
155156
});
156-
await npmInstallationManager.install("tns-template-hello-world", defaultTemplateDir, { dependencyType: "save" });
157-
defaultTemplatePath = path.join(defaultTemplateDir, "node_modules", "tns-template-hello-world");
157+
158+
await npmInstallationManager.install(constants.RESERVED_TEMPLATE_NAMES["default"], defaultTemplateDir, { dependencyType: "save" });
159+
defaultTemplatePath = path.join(defaultTemplateDir, "node_modules", constants.RESERVED_TEMPLATE_NAMES["default"]);
160+
158161
fs.deleteDirectory(path.join(defaultTemplatePath, "node_modules"));
159162

160163
let defaultSpecificVersionTemplateDir = temp.mkdirSync("defaultTemplateSpeciffic");
@@ -166,8 +169,10 @@ describe("Project Service Tests", () => {
166169
"readme": "dummy",
167170
"repository": "dummy"
168171
});
169-
await npmInstallationManager.install("tns-template-hello-world", defaultSpecificVersionTemplateDir, { version: "1.4.0", dependencyType: "save" });
170-
defaultSpecificVersionTemplatePath = path.join(defaultSpecificVersionTemplateDir, "node_modules", "tns-template-hello-world");
172+
173+
await npmInstallationManager.install(constants.RESERVED_TEMPLATE_NAMES["default"], defaultSpecificVersionTemplateDir, { version: "1.4.0", dependencyType: "save" });
174+
defaultSpecificVersionTemplatePath = path.join(defaultSpecificVersionTemplateDir, "node_modules", constants.RESERVED_TEMPLATE_NAMES["default"]);
175+
171176
fs.deleteDirectory(path.join(defaultSpecificVersionTemplatePath, "node_modules"));
172177

173178
let angularTemplateDir = temp.mkdirSync("angularTemplate");
@@ -179,8 +184,10 @@ describe("Project Service Tests", () => {
179184
"readme": "dummy",
180185
"repository": "dummy"
181186
});
182-
await npmInstallationManager.install("tns-template-hello-world-ng", angularTemplateDir, { dependencyType: "save" });
183-
angularTemplatePath = path.join(angularTemplateDir, "node_modules", "tns-template-hello-world-ng");
187+
188+
await npmInstallationManager.install(constants.RESERVED_TEMPLATE_NAMES["angular"], angularTemplateDir, { dependencyType: "save" });
189+
angularTemplatePath = path.join(angularTemplateDir, "node_modules", constants.RESERVED_TEMPLATE_NAMES["angular"]);
190+
184191
fs.deleteDirectory(path.join(angularTemplatePath, "node_modules"));
185192

186193
let typescriptTemplateDir = temp.mkdirSync("typescriptTemplate");
@@ -192,8 +199,10 @@ describe("Project Service Tests", () => {
192199
"readme": "dummy",
193200
"repository": "dummy"
194201
});
195-
await npmInstallationManager.install("tns-template-hello-world-ts", typescriptTemplateDir, { dependencyType: "save" });
196-
typescriptTemplatePath = path.join(typescriptTemplateDir, "node_modules", "tns-template-hello-world-ts");
202+
203+
await npmInstallationManager.install(constants.RESERVED_TEMPLATE_NAMES["typescript"], typescriptTemplateDir, { dependencyType: "save" });
204+
typescriptTemplatePath = path.join(typescriptTemplateDir, "node_modules", constants.RESERVED_TEMPLATE_NAMES["typescript"]);
205+
197206
fs.deleteDirectory(path.join(typescriptTemplatePath, "node_modules"));
198207
});
199208

@@ -436,6 +445,7 @@ function createTestInjector() {
436445
testInjector.register("projectDataService", ProjectDataServiceLib.ProjectDataService);
437446

438447
testInjector.register("staticConfig", StaticConfig);
448+
testInjector.register("analyticsService", { track: async () => undefined });
439449

440450
testInjector.register("npmInstallationManager", NpmInstallationManager);
441451
testInjector.register("httpClient", HttpClientLib.HttpClient);

0 commit comments

Comments
 (0)