diff --git a/CHANGELOG.md b/CHANGELOG.md index 12f62a7389..17baddd493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ NativeScript CLI Changelog ================ +4.0.2 (2018, May 18) +== + +### Fixed +* [Fixed #3595](https://github.com/NativeScript/nativescript-cli/issues/3595): Do not track local paths in Analytics +* [Fixed #3597](https://github.com/NativeScript/nativescript-cli/issues/3597): Users who subscribe to Progess Newsletter are not informed for the privacy policy + 4.0.1 (2018, May 11) == diff --git a/lib/constants.ts b/lib/constants.ts index 63c7001f37..6a52e9788e 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -1,3 +1,5 @@ +require("colors"); + export const APP_FOLDER_NAME = "app"; export const APP_RESOURCES_FOLDER_NAME = "App_Resources"; export const PROJECT_FRAMEWORK_FOLDER_NAME = "framework"; @@ -78,6 +80,8 @@ export const RESERVED_TEMPLATE_NAMES: IStringDictionary = { "angular": "tns-template-hello-world-ng" }; +export const ANALYTICS_LOCAL_TEMPLATE_PREFIX = "localTemplate_"; + export class ITMSConstants { static ApplicationMetadataFile = "metadata.xml"; static VerboseLoggingLevels = { @@ -178,3 +182,10 @@ export class MacOSVersions { } export const MacOSDeprecationStringFormat = "Support for macOS %s is deprecated and will be removed in one of the next releases of NativeScript. Please, upgrade to the latest macOS version."; +export const PROGRESS_PRIVACY_POLICY_URL = "https://www.progress.com/legal/privacy-policy"; +export class SubscribeForNewsletterMessages { + public static AgreeToReceiveEmailMsg = "I agree to receive email communications from Progress Software or its Partners (`https://www.progress.com/partners/partner-directory`)," + + "containing information about Progress Software's products. Consent may be withdrawn at any time."; + public static ReviewPrivacyPolicyMsg = `You can review the Progress Software Privacy Policy at \`${PROGRESS_PRIVACY_POLICY_URL}\``; + public static PromptMsg = "Input your e-mail address to agree".green + " or " + "leave empty to decline".red.bold + ":"; +} diff --git a/lib/services/project-templates-service.ts b/lib/services/project-templates-service.ts index 32ac27ec57..632b8afb30 100644 --- a/lib/services/project-templates-service.ts +++ b/lib/services/project-templates-service.ts @@ -18,15 +18,18 @@ export class ProjectTemplatesService implements IProjectTemplatesService { const templateName = constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()] || name; - await this.$analyticsService.track("Template used for project creation", templateName); + const realTemplatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir); - await this.$analyticsService.trackEventActionInGoogleAnalytics({ - action: constants.TrackActionNames.CreateProject, - isForDevice: null, - additionalData: templateName - }); + await this.$analyticsService.track("Template used for project creation", templateName); - const realTemplatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir); + const templateNameToBeTracked = this.getTemplateNameToBeTracked(templateName, realTemplatePath); + if (templateNameToBeTracked) { + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: constants.TrackActionNames.CreateProject, + isForDevice: null, + additionalData: templateNameToBeTracked + }); + } // this removes dependencies from templates so they are not copied to app folder this.$fs.deleteDirectory(path.join(realTemplatePath, constants.NODE_MODULES_FOLDER_NAME)); @@ -46,5 +49,25 @@ export class ProjectTemplatesService implements IProjectTemplatesService { this.$logger.trace(`Using NativeScript verified template: ${templateName} with version ${version}.`); return this.$npmInstallationManager.install(templateName, projectDir, { version: version, dependencyType: "save" }); } + + private getTemplateNameToBeTracked(templateName: string, realTemplatePath: string): string { + try { + if (this.$fs.exists(templateName)) { + // local template is used + const pathToPackageJson = path.join(realTemplatePath, constants.PACKAGE_JSON_FILE_NAME); + let templateNameToTrack = path.basename(templateName); + if (this.$fs.exists(pathToPackageJson)) { + const templatePackageJsonContent = this.$fs.readJson(pathToPackageJson); + templateNameToTrack = templatePackageJsonContent.name; + } + + return `${constants.ANALYTICS_LOCAL_TEMPLATE_PREFIX}${templateNameToTrack}`; + } + + return templateName; + } catch (err) { + this.$logger.trace(`Unable to get template name to be tracked, error is: ${err}`); + } + } } $injector.register("projectTemplatesService", ProjectTemplatesService); diff --git a/lib/services/subscription-service.ts b/lib/services/subscription-service.ts index e1302d865a..7c28a2aa56 100644 --- a/lib/services/subscription-service.ts +++ b/lib/services/subscription-service.ts @@ -1,6 +1,7 @@ import * as emailValidator from "email-validator"; import * as queryString from "querystring"; import * as helpers from "../common/helpers"; +import { SubscribeForNewsletterMessages } from "../constants"; export class SubscriptionService implements ISubscriptionService { constructor(private $httpClient: Server.IHttpClient, @@ -11,8 +12,10 @@ export class SubscriptionService implements ISubscriptionService { public async subscribeForNewsletter(): Promise { if (await this.shouldAskForEmail()) { - this.$logger.out("Enter your e-mail address to subscribe to the NativeScript Newsletter and hear about product updates, tips & tricks, and community happenings:"); - const email = await this.getEmail("(press Enter for blank)"); + this.$logger.printMarkdown(SubscribeForNewsletterMessages.AgreeToReceiveEmailMsg); + this.$logger.printMarkdown(SubscribeForNewsletterMessages.ReviewPrivacyPolicyMsg); + + const email = await this.getEmail(SubscribeForNewsletterMessages.PromptMsg); await this.$userSettingsService.saveSetting("EMAIL_REGISTERED", true); await this.sendEmail(email); } diff --git a/test/project-templates-service.ts b/test/project-templates-service.ts index e227a232e3..5ee01a0fbe 100644 --- a/test/project-templates-service.ts +++ b/test/project-templates-service.ts @@ -3,7 +3,6 @@ import * as stubs from "./stubs"; import { ProjectTemplatesService } from "../lib/services/project-templates-service"; import { assert } from "chai"; import * as path from "path"; -import temp = require("temp"); import * as constants from "../lib/constants"; let isDeleteDirectoryCalledForNodeModulesDir = false; @@ -25,9 +24,12 @@ function createTestInjector(configuration?: { shouldNpmInstallThrow: boolean, np if (directory.indexOf("node_modules") !== -1) { isDeleteDirectoryCalledForNodeModulesDir = true; } - } + }, + + exists: (filePath: string): boolean => false }); + injector.register("npm", { install: (packageName: string, pathToSave: string, config?: any) => { if (configuration.shouldNpmInstallThrow) { @@ -70,8 +72,7 @@ describe("project-templates-service", () => { it("when npm install fails", async () => { testInjector = createTestInjector({ shouldNpmInstallThrow: true, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: null }); projectTemplatesService = testInjector.resolve("projectTemplatesService"); - const tempFolder = temp.mkdirSync("preparetemplate"); - await assert.isRejected(projectTemplatesService.prepareTemplate("invalidName", tempFolder)); + await assert.isRejected(projectTemplatesService.prepareTemplate("invalidName", "tempFolder")); }); }); @@ -79,8 +80,7 @@ describe("project-templates-service", () => { it("when reserved template name is used", async () => { testInjector = createTestInjector({ shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: [] }); projectTemplatesService = testInjector.resolve("projectTemplatesService"); - const tempFolder = temp.mkdirSync("preparetemplate"); - const actualPathToTemplate = await projectTemplatesService.prepareTemplate("typescript", tempFolder); + const actualPathToTemplate = await projectTemplatesService.prepareTemplate("typescript", "tempFolder"); assert.strictEqual(path.basename(actualPathToTemplate), nativeScriptValidatedTemplatePath); assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted."); }); @@ -88,8 +88,7 @@ describe("project-templates-service", () => { it("when reserved template name is used (case-insensitive test)", async () => { testInjector = createTestInjector({ shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: [] }); projectTemplatesService = testInjector.resolve("projectTemplatesService"); - const tempFolder = temp.mkdirSync("preparetemplate"); - const actualPathToTemplate = await projectTemplatesService.prepareTemplate("tYpEsCriPT", tempFolder); + const actualPathToTemplate = await projectTemplatesService.prepareTemplate("tYpEsCriPT", "tempFolder"); assert.strictEqual(path.basename(actualPathToTemplate), nativeScriptValidatedTemplatePath); assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted."); }); @@ -97,11 +96,75 @@ describe("project-templates-service", () => { it("uses defaultTemplate when undefined is passed as parameter", async () => { testInjector = createTestInjector({ shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: [] }); projectTemplatesService = testInjector.resolve("projectTemplatesService"); - const tempFolder = temp.mkdirSync("preparetemplate"); - const actualPathToTemplate = await projectTemplatesService.prepareTemplate(constants.RESERVED_TEMPLATE_NAMES["default"], tempFolder); + const actualPathToTemplate = await projectTemplatesService.prepareTemplate(constants.RESERVED_TEMPLATE_NAMES["default"], "tempFolder"); assert.strictEqual(path.basename(actualPathToTemplate), nativeScriptValidatedTemplatePath); assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted."); }); }); + + describe("sends correct information to Google Analytics", () => { + let analyticsService: IAnalyticsService; + let dataSentToGoogleAnalytics: IEventActionData; + beforeEach(() => { + testInjector = createTestInjector({ shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: [] }); + analyticsService = testInjector.resolve("analyticsService"); + dataSentToGoogleAnalytics = null; + analyticsService.trackEventActionInGoogleAnalytics = async (data: IEventActionData): Promise => { + dataSentToGoogleAnalytics = data; + }; + projectTemplatesService = testInjector.resolve("projectTemplatesService"); + }); + + it("sends template name when the template is used from npm", async () => { + const templateName = "template-from-npm"; + await projectTemplatesService.prepareTemplate(templateName, "tempFolder"); + assert.deepEqual(dataSentToGoogleAnalytics, { + action: constants.TrackActionNames.CreateProject, + isForDevice: null, + additionalData: templateName + }); + }); + + it("sends template name (from template's package.json) when the template is used from local path", async () => { + const templateName = "my-own-local-template"; + const localTemplatePath = "/Users/username/localtemplate"; + const fs = testInjector.resolve("fs"); + fs.exists = (path: string): boolean => true; + fs.readJson = (filename: string, encoding?: string): any => ({ name: templateName }); + await projectTemplatesService.prepareTemplate(localTemplatePath, "tempFolder"); + assert.deepEqual(dataSentToGoogleAnalytics, { + action: constants.TrackActionNames.CreateProject, + isForDevice: null, + additionalData: `${constants.ANALYTICS_LOCAL_TEMPLATE_PREFIX}${templateName}` + }); + }); + + it("sends the template name (path to dirname) when the template is used from local path but there's no package.json at the root", async () => { + const templateName = "localtemplate"; + const localTemplatePath = `/Users/username/${templateName}`; + const fs = testInjector.resolve("fs"); + fs.exists = (localPath: string): boolean => path.basename(localPath) !== constants.PACKAGE_JSON_FILE_NAME; + await projectTemplatesService.prepareTemplate(localTemplatePath, "tempFolder"); + assert.deepEqual(dataSentToGoogleAnalytics, { + action: constants.TrackActionNames.CreateProject, + isForDevice: null, + additionalData: `${constants.ANALYTICS_LOCAL_TEMPLATE_PREFIX}${templateName}` + }); + }); + + it("does not send anything when trying to get template name fails", async () => { + const templateName = "localtemplate"; + const localTemplatePath = `/Users/username/${templateName}`; + const fs = testInjector.resolve("fs"); + fs.exists = (localPath: string): boolean => true; + fs.readJson = (filename: string, encoding?: string): any => { + throw new Error("Unable to read json"); + }; + + await projectTemplatesService.prepareTemplate(localTemplatePath, "tempFolder"); + + assert.deepEqual(dataSentToGoogleAnalytics, null); + }); + }); }); }); diff --git a/test/services/subscription-service.ts b/test/services/subscription-service.ts index 6223e531a9..4e8774864b 100644 --- a/test/services/subscription-service.ts +++ b/test/services/subscription-service.ts @@ -3,6 +3,7 @@ import { assert } from "chai"; import { SubscriptionService } from "../../lib/services/subscription-service"; import { LoggerStub } from "../stubs"; import { stringify } from "querystring"; +import { SubscribeForNewsletterMessages } from "../../lib/constants"; const helpers = require("../../lib/common/helpers"); interface IValidateTestData { @@ -153,12 +154,16 @@ describe("subscriptionService", () => { loggerOutput += args.join(" "); }; + logger.printMarkdown = (message: string): void => { + loggerOutput += message; + }; + await subscriptionService.subscribeForNewsletter(); - assert.equal(loggerOutput, "Enter your e-mail address to subscribe to the NativeScript Newsletter and hear about product updates, tips & tricks, and community happenings:"); + assert.equal(loggerOutput, `${SubscribeForNewsletterMessages.AgreeToReceiveEmailMsg}${SubscribeForNewsletterMessages.ReviewPrivacyPolicyMsg}`); }); - const expectedMessageInPrompter = "(press Enter for blank)"; + const expectedMessageInPrompter = SubscribeForNewsletterMessages.PromptMsg; it(`calls prompter with specific message - ${expectedMessageInPrompter}`, async () => { const testInjector = createTestInjector(); const subscriptionService = testInjector.resolve(SubscriptionServiceTester);