Skip to content

Commit 0d69950

Browse files
Add tracking for the project types that users are creating and working with (#2492)
* 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 efcdd19 commit 0d69950

13 files changed

+229
-53
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): IFuture<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 {IFuture<void>}
151+
*/
152+
trackProjectType(): IFuture<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
@@ -10,8 +10,10 @@ interface IProjectData {
1010
projectFilePath: string;
1111
projectId?: string;
1212
dependencies: any;
13+
devDependencies: IStringDictionary;
1314
appDirectoryPath: string;
1415
appResourcesDirectoryPath: string;
16+
projectType: string;
1517
}
1618

1719
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
@@ -101,6 +101,8 @@ class LiveSyncService implements ILiveSyncService {
101101
@helpers.hook('livesync')
102102
private liveSyncCore(liveSyncData: ILiveSyncData[], applicationReloadAction: (deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture<void>): IFuture<void> {
103103
return (() => {
104+
this.$platformService.trackProjectType().wait();
105+
104106
let watchForChangeActions: ((event: string, filePath: string, dispatcher: IFutureDispatcher) => void)[] = [];
105107
_.each(liveSyncData, (dataItem) => {
106108
let service: IPlatformLiveSyncService = this.$injector.resolve("platformLiveSyncService", { _liveSyncData: dataItem });

lib/services/platform-service.ts

+26-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ let clui = require("clui");
1212
const buildInfoFileName = ".nsbuildinfo";
1313

1414
export class PlatformService implements IPlatformService {
15+
private _trackedProjectFilePath: string = null;
1516

1617
constructor(private $devicesService: Mobile.IDevicesService,
1718
private $errors: IErrors,
@@ -38,7 +39,8 @@ export class PlatformService implements IPlatformService {
3839
private $deviceAppDataFactory: Mobile.IDeviceAppDataFactory,
3940
private $projectChangesService: IProjectChangesService,
4041
private $emulatorPlatformService: IEmulatorPlatformService,
41-
private $childProcess: IChildProcess) { }
42+
private $childProcess: IChildProcess,
43+
private $analyticsService: IAnalyticsService) { }
4244

4345
public addPlatforms(platforms: string[]): IFuture<void> {
4446
return (() => {
@@ -196,6 +198,8 @@ export class PlatformService implements IPlatformService {
196198
return (() => {
197199
this.validatePlatform(platform);
198200

201+
this.trackProjectType().wait();
202+
199203
//We need dev-dependencies here, so before-prepare hooks will be executed correctly.
200204
try {
201205
this.$pluginsService.ensureAllDependenciesAreInstalled().wait();
@@ -348,7 +352,7 @@ export class PlatformService implements IPlatformService {
348352
}
349353
let platformData = this.$platformsData.getPlatformData(platform);
350354
let forDevice = !buildConfig || buildConfig.buildForDevice;
351-
let outputPath = forDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath;
355+
let outputPath = forDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath;
352356
if (!this.$fs.exists(outputPath)) {
353357
return true;
354358
}
@@ -372,9 +376,24 @@ export class PlatformService implements IPlatformService {
372376
}).future<boolean>()();
373377
}
374378

379+
public trackProjectType(): IFuture<void> {
380+
return (() => {
381+
// Track each project once per process.
382+
// In long living process, where we may work with multiple projects, we would like to track the information for each of them.
383+
if (this.$projectData && (this.$projectData.projectFilePath !== this._trackedProjectFilePath)) {
384+
this._trackedProjectFilePath = this.$projectData.projectFilePath;
385+
386+
this.$analyticsService.track("Working with project type", this.$projectData.projectType).wait();
387+
}
388+
}).future<void>()();
389+
}
390+
375391
public buildPlatform(platform: string, buildConfig?: IBuildConfig): IFuture<void> {
376392
return (() => {
377393
this.$logger.out("Building project...");
394+
395+
this.trackProjectType().wait();
396+
378397
let platformData = this.$platformsData.getPlatformData(platform);
379398
platformData.platformProjectService.buildProject(platformData.projectRoot, buildConfig).wait();
380399
let prepareInfo = this.$projectChangesService.getPrepareInfo(platform);
@@ -449,6 +468,8 @@ export class PlatformService implements IPlatformService {
449468

450469
public runPlatform(platform: string): IFuture<void> {
451470
return (() => {
471+
this.trackProjectType().wait();
472+
452473
if (this.$options.justlaunch) {
453474
this.$options.watch = false;
454475
}
@@ -484,7 +505,7 @@ export class PlatformService implements IPlatformService {
484505
this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }).wait();
485506
let found: Mobile.IDeviceInfo[] = [];
486507
if (this.$devicesService.hasDevices) {
487-
found = this.$devicesService.getDevices().filter((device:Mobile.IDeviceInfo) => device.identifier === this.$options.device);
508+
found = this.$devicesService.getDevices().filter((device: Mobile.IDeviceInfo) => device.identifier === this.$options.device);
488509
}
489510
if (found.length === 0) {
490511
this.$errors.fail("Cannot find device with name: %s", this.$options.device);
@@ -514,7 +535,7 @@ export class PlatformService implements IPlatformService {
514535
let deviceFilePath = this.getDeviceBuildInfoFilePath(device);
515536
try {
516537
return JSON.parse(this.readFile(device, deviceFilePath).wait());
517-
} catch(e) {
538+
} catch (e) {
518539
return null;
519540
};
520541
}).future<IBuildInfo>()();
@@ -527,7 +548,7 @@ export class PlatformService implements IPlatformService {
527548
try {
528549
let buildInfoTime = this.$fs.readJson(buildInfoFile);
529550
return buildInfoTime;
530-
} catch(e) {
551+
} catch (e) {
531552
return null;
532553
}
533554
}

lib/services/project-templates-service.ts

+20-19
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,32 @@ temp.track();
66
export class ProjectTemplatesService implements IProjectTemplatesService {
77

88
public constructor(private $errors: IErrors,
9-
private $fs: IFileSystem,
10-
private $logger: ILogger,
11-
private $npm: INodePackageManager,
12-
private $npmInstallationManager: INpmInstallationManager) { }
9+
private $fs: IFileSystem,
10+
private $logger: ILogger,
11+
private $npm: INodePackageManager,
12+
private $npmInstallationManager: INpmInstallationManager,
13+
private $analyticsService: IAnalyticsService) { }
1314

1415
public prepareTemplate(originalTemplateName: string, projectDir: string): IFuture<string> {
1516
return ((): string => {
16-
let realTemplatePath: string;
17-
if(originalTemplateName) {
17+
let templateName = originalTemplateName || constants.RESERVED_TEMPLATE_NAMES["default"],
18+
version: string = null;
19+
20+
if (originalTemplateName) {
1821
// support <reserved_name>@<version> syntax
1922
let data = originalTemplateName.split("@"),
20-
name = data[0],
21-
version = data[1];
22-
23-
if(constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()]) {
24-
realTemplatePath = this.prepareNativeScriptTemplate(constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()], version, projectDir).wait();
25-
} else {
26-
// Use the original template name, specified by user as it may be case-sensitive.
27-
realTemplatePath = this.prepareNativeScriptTemplate(name, version, projectDir).wait();
28-
}
29-
} else {
30-
realTemplatePath = this.prepareNativeScriptTemplate(constants.RESERVED_TEMPLATE_NAMES["default"], null/*version*/, projectDir).wait();
23+
name = data[0];
24+
25+
version = data[1];
26+
27+
templateName = constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()] || name;
3128
}
3229

33-
if(realTemplatePath) {
30+
this.$analyticsService.track("Template used for project creation", templateName).wait();
31+
32+
const realTemplatePath = this.prepareNativeScriptTemplate(templateName, version, projectDir).wait();
33+
34+
if (realTemplatePath) {
3435
//this removes dependencies from templates so they are not copied to app folder
3536
this.$fs.deleteDirectory(path.join(realTemplatePath, constants.NODE_MODULES_FOLDER_NAME));
3637
return realTemplatePath;
@@ -51,7 +52,7 @@ export class ProjectTemplatesService implements IProjectTemplatesService {
5152
*/
5253
private prepareNativeScriptTemplate(templateName: string, version?: string, projectDir?: string): IFuture<string> {
5354
this.$logger.trace(`Using NativeScript verified template: ${templateName} with version ${version}.`);
54-
return this.$npmInstallationManager.install(templateName, projectDir, {version: version, dependencyType: "save"});
55+
return this.$npmInstallationManager.install(templateName, projectDir, { version: version, dependencyType: "save" });
5556
}
5657
}
5758
$injector.register("projectTemplatesService", ProjectTemplatesService);

test/npm-support.ts

+4
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ function createTestInjector(): IInjector {
8080
testInjector.register("config", StaticConfigLib.Configuration);
8181
testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService);
8282
testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService);
83+
testInjector.register("analyticsService", {
84+
track: () => Future.fromResult()
85+
});
86+
8387
return testInjector;
8488
}
8589

test/platform-commands.ts

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { XmlValidator } from "../lib/xml-validator";
2121
import * as ChildProcessLib from "../lib/common/child-process";
2222
import {CleanCommand} from "../lib/commands/platform-clean";
2323
import ProjectChangesLib = require("../lib/services/project-changes-service");
24+
import Future = require("fibers/future");
2425

2526
let isCommandExecuted = true;
2627

@@ -142,6 +143,10 @@ function createTestInjector() {
142143
testInjector.register("childProcess", ChildProcessLib.ChildProcess);
143144
testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService);
144145
testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService);
146+
testInjector.register("analyticsService", {
147+
track: () => Future.fromResult()
148+
});
149+
145150
return testInjector;
146151
}
147152

test/platform-service.ts

+3
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ function createTestInjector() {
8181
testInjector.register("childProcess", ChildProcessLib.ChildProcess);
8282
testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService);
8383
testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService);
84+
testInjector.register("analyticsService", {
85+
track: () => Future.fromResult()
86+
});
8487

8588
return testInjector;
8689
}

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+
});

0 commit comments

Comments
 (0)