Skip to content

Commit f536725

Browse files
fix(GDPR): Do not track local project paths
Do not track paths to projects when local template is used. Instead track `localTemplate_<name from template's package.json or dirname>`. Also fix the prompter for emails as we need to show information how to unsubscribe from the newsletter. Add tests for the behavior.
1 parent 7ce030c commit f536725

File tree

5 files changed

+100
-17
lines changed

5 files changed

+100
-17
lines changed

lib/constants.ts

+10
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ export const RESERVED_TEMPLATE_NAMES: IStringDictionary = {
7878
"angular": "tns-template-hello-world-ng"
7979
};
8080

81+
export const ANALYTICS_LOCAL_TEMPLATE_PREFIX = "localTemplate_";
82+
8183
export class ITMSConstants {
8284
static ApplicationMetadataFile = "metadata.xml";
8385
static VerboseLoggingLevels = {
@@ -170,3 +172,11 @@ export class AssetConstants {
170172
public static defaultScale = 1;
171173
public static defaultOverlayImageScale = 0.8;
172174
}
175+
176+
export const PROGRESS_PRIVACY_POLICY_URL = "https://www.progress.com/legal/privacy-policy";
177+
export class SubscribeForNewsletterMessages {
178+
public static AgreeToReceiveEmailMsg = "I agree to receive email communications from Progress Software or its Partners (`https://www.progress.com/partners/partner-directory`)," +
179+
"containing information about Progress Software's products. Consent may be withdrawn at any time.";
180+
public static ReviewPrivacyPolicyMsg = `You can review the Progress Software Privacy Policy at \`${PROGRESS_PRIVACY_POLICY_URL}\``;
181+
public static PromptMsg = "Input your e-mail address to agree".green + " or " + "leave empty to decline".red.bold + ":";
182+
}

lib/services/project-templates-service.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ export class ProjectTemplatesService implements IProjectTemplatesService {
1818

1919
const templateName = constants.RESERVED_TEMPLATE_NAMES[name.toLowerCase()] || name;
2020

21+
const realTemplatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir);
22+
2123
await this.$analyticsService.track("Template used for project creation", templateName);
2224

2325
await this.$analyticsService.trackEventActionInGoogleAnalytics({
2426
action: constants.TrackActionNames.CreateProject,
2527
isForDevice: null,
26-
additionalData: templateName
28+
additionalData: this.getTemplateNameToBeTracked(templateName, realTemplatePath)
2729
});
2830

29-
const realTemplatePath = await this.prepareNativeScriptTemplate(templateName, version, projectDir);
30-
3131
// this removes dependencies from templates so they are not copied to app folder
3232
this.$fs.deleteDirectory(path.join(realTemplatePath, constants.NODE_MODULES_FOLDER_NAME));
3333

@@ -46,5 +46,21 @@ export class ProjectTemplatesService implements IProjectTemplatesService {
4646
this.$logger.trace(`Using NativeScript verified template: ${templateName} with version ${version}.`);
4747
return this.$npmInstallationManager.install(templateName, projectDir, { version: version, dependencyType: "save" });
4848
}
49+
50+
private getTemplateNameToBeTracked(templateName: string, realTemplatePath: string): string {
51+
if (this.$fs.exists(templateName)) {
52+
// local template is used
53+
const pathToPackageJson = path.join(realTemplatePath, constants.PACKAGE_JSON_FILE_NAME);
54+
let templateNameToTrack = path.basename(templateName);
55+
if (this.$fs.exists(pathToPackageJson)) {
56+
const templatePackageJsonContent = this.$fs.readJson(pathToPackageJson);
57+
templateNameToTrack = templatePackageJsonContent.name;
58+
}
59+
60+
return `${constants.ANALYTICS_LOCAL_TEMPLATE_PREFIX}${templateNameToTrack}`;
61+
}
62+
63+
return templateName;
64+
}
4965
}
5066
$injector.register("projectTemplatesService", ProjectTemplatesService);

lib/services/subscription-service.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as emailValidator from "email-validator";
22
import * as queryString from "querystring";
33
import * as helpers from "../common/helpers";
4+
import { SubscribeForNewsletterMessages } from "../constants";
45

56
export class SubscriptionService implements ISubscriptionService {
67
constructor(private $httpClient: Server.IHttpClient,
@@ -11,8 +12,10 @@ export class SubscriptionService implements ISubscriptionService {
1112

1213
public async subscribeForNewsletter(): Promise<void> {
1314
if (await this.shouldAskForEmail()) {
14-
this.$logger.out("Enter your e-mail address to subscribe to the NativeScript Newsletter and hear about product updates, tips & tricks, and community happenings:");
15-
const email = await this.getEmail("(press Enter for blank)");
15+
this.$logger.printMarkdown(SubscribeForNewsletterMessages.AgreeToReceiveEmailMsg);
16+
this.$logger.printMarkdown(SubscribeForNewsletterMessages.ReviewPrivacyPolicyMsg);
17+
18+
const email = await this.getEmail(SubscribeForNewsletterMessages.PromptMsg);
1619
await this.$userSettingsService.saveSetting("EMAIL_REGISTERED", true);
1720
await this.sendEmail(email);
1821
}

test/project-templates-service.ts

+59-10
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import * as stubs from "./stubs";
33
import { ProjectTemplatesService } from "../lib/services/project-templates-service";
44
import { assert } from "chai";
55
import * as path from "path";
6-
import temp = require("temp");
76
import * as constants from "../lib/constants";
87

98
let isDeleteDirectoryCalledForNodeModulesDir = false;
@@ -25,9 +24,12 @@ function createTestInjector(configuration?: { shouldNpmInstallThrow: boolean, np
2524
if (directory.indexOf("node_modules") !== -1) {
2625
isDeleteDirectoryCalledForNodeModulesDir = true;
2726
}
28-
}
27+
},
28+
29+
exists: (filePath: string): boolean => false
2930

3031
});
32+
3133
injector.register("npm", {
3234
install: (packageName: string, pathToSave: string, config?: any) => {
3335
if (configuration.shouldNpmInstallThrow) {
@@ -70,38 +72,85 @@ describe("project-templates-service", () => {
7072
it("when npm install fails", async () => {
7173
testInjector = createTestInjector({ shouldNpmInstallThrow: true, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: null });
7274
projectTemplatesService = testInjector.resolve("projectTemplatesService");
73-
const tempFolder = temp.mkdirSync("preparetemplate");
74-
await assert.isRejected(projectTemplatesService.prepareTemplate("invalidName", tempFolder));
75+
await assert.isRejected(projectTemplatesService.prepareTemplate("invalidName", "tempFolder"));
7576
});
7677
});
7778

7879
describe("returns correct path to template", () => {
7980
it("when reserved template name is used", async () => {
8081
testInjector = createTestInjector({ shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: [] });
8182
projectTemplatesService = testInjector.resolve("projectTemplatesService");
82-
const tempFolder = temp.mkdirSync("preparetemplate");
83-
const actualPathToTemplate = await projectTemplatesService.prepareTemplate("typescript", tempFolder);
83+
const actualPathToTemplate = await projectTemplatesService.prepareTemplate("typescript", "tempFolder");
8484
assert.strictEqual(path.basename(actualPathToTemplate), nativeScriptValidatedTemplatePath);
8585
assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted.");
8686
});
8787

8888
it("when reserved template name is used (case-insensitive test)", async () => {
8989
testInjector = createTestInjector({ shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: [] });
9090
projectTemplatesService = testInjector.resolve("projectTemplatesService");
91-
const tempFolder = temp.mkdirSync("preparetemplate");
92-
const actualPathToTemplate = await projectTemplatesService.prepareTemplate("tYpEsCriPT", tempFolder);
91+
const actualPathToTemplate = await projectTemplatesService.prepareTemplate("tYpEsCriPT", "tempFolder");
9392
assert.strictEqual(path.basename(actualPathToTemplate), nativeScriptValidatedTemplatePath);
9493
assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted.");
9594
});
9695

9796
it("uses defaultTemplate when undefined is passed as parameter", async () => {
9897
testInjector = createTestInjector({ shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: [] });
9998
projectTemplatesService = testInjector.resolve("projectTemplatesService");
100-
const tempFolder = temp.mkdirSync("preparetemplate");
101-
const actualPathToTemplate = await projectTemplatesService.prepareTemplate(constants.RESERVED_TEMPLATE_NAMES["default"], tempFolder);
99+
const actualPathToTemplate = await projectTemplatesService.prepareTemplate(constants.RESERVED_TEMPLATE_NAMES["default"], "tempFolder");
102100
assert.strictEqual(path.basename(actualPathToTemplate), nativeScriptValidatedTemplatePath);
103101
assert.strictEqual(isDeleteDirectoryCalledForNodeModulesDir, true, "When correct path is returned, template's node_modules directory should be deleted.");
104102
});
105103
});
104+
105+
describe("sends correct information to Google Analytics", () => {
106+
let analyticsService: IAnalyticsService;
107+
let dataSentToGoogleAnalytics: IEventActionData;
108+
beforeEach(() => {
109+
testInjector = createTestInjector({ shouldNpmInstallThrow: false, npmInstallationDirContents: [], npmInstallationDirNodeModulesContents: [] });
110+
analyticsService = testInjector.resolve<IAnalyticsService>("analyticsService");
111+
dataSentToGoogleAnalytics = null;
112+
analyticsService.trackEventActionInGoogleAnalytics = async (data: IEventActionData): Promise<void> => {
113+
dataSentToGoogleAnalytics = data;
114+
};
115+
projectTemplatesService = testInjector.resolve("projectTemplatesService");
116+
});
117+
118+
it("sends template name when the template is used from npm", async () => {
119+
const templateName = "template-from-npm";
120+
await projectTemplatesService.prepareTemplate(templateName, "tempFolder");
121+
assert.deepEqual(dataSentToGoogleAnalytics, {
122+
action: constants.TrackActionNames.CreateProject,
123+
isForDevice: null,
124+
additionalData: templateName
125+
});
126+
});
127+
128+
it("sends template name (from template's package.json) when the template is used from local path", async () => {
129+
const templateName = "my-own-local-template";
130+
const localTemplatePath = "/Users/username/localtemplate";
131+
const fs = testInjector.resolve<IFileSystem>("fs");
132+
fs.exists = (path: string): boolean => true;
133+
fs.readJson = (filename: string, encoding?: string): any => ({ name: templateName });
134+
await projectTemplatesService.prepareTemplate(localTemplatePath, "tempFolder");
135+
assert.deepEqual(dataSentToGoogleAnalytics, {
136+
action: constants.TrackActionNames.CreateProject,
137+
isForDevice: null,
138+
additionalData: `${constants.ANALYTICS_LOCAL_TEMPLATE_PREFIX}${templateName}`
139+
});
140+
});
141+
142+
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 () => {
143+
const templateName = "localtemplate";
144+
const localTemplatePath = `/Users/username/${templateName}`;
145+
const fs = testInjector.resolve<IFileSystem>("fs");
146+
fs.exists = (localPath: string): boolean => path.basename(localPath) !== constants.PACKAGE_JSON_FILE_NAME;
147+
await projectTemplatesService.prepareTemplate(localTemplatePath, "tempFolder");
148+
assert.deepEqual(dataSentToGoogleAnalytics, {
149+
action: constants.TrackActionNames.CreateProject,
150+
isForDevice: null,
151+
additionalData: `${constants.ANALYTICS_LOCAL_TEMPLATE_PREFIX}${templateName}`
152+
});
153+
});
154+
});
106155
});
107156
});

test/services/subscription-service.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { assert } from "chai";
33
import { SubscriptionService } from "../../lib/services/subscription-service";
44
import { LoggerStub } from "../stubs";
55
import { stringify } from "querystring";
6+
import { SubscribeForNewsletterMessages } from "../../lib/constants";
67
const helpers = require("../../lib/common/helpers");
78

89
interface IValidateTestData {
@@ -153,12 +154,16 @@ describe("subscriptionService", () => {
153154
loggerOutput += args.join(" ");
154155
};
155156

157+
logger.printMarkdown = (message: string): void => {
158+
loggerOutput += message;
159+
};
160+
156161
await subscriptionService.subscribeForNewsletter();
157162

158-
assert.equal(loggerOutput, "Enter your e-mail address to subscribe to the NativeScript Newsletter and hear about product updates, tips & tricks, and community happenings:");
163+
assert.equal(loggerOutput, `${SubscribeForNewsletterMessages.AgreeToReceiveEmailMsg}${SubscribeForNewsletterMessages.ReviewPrivacyPolicyMsg}`);
159164
});
160165

161-
const expectedMessageInPrompter = "(press Enter for blank)";
166+
const expectedMessageInPrompter = SubscribeForNewsletterMessages.PromptMsg;
162167
it(`calls prompter with specific message - ${expectedMessageInPrompter}`, async () => {
163168
const testInjector = createTestInjector();
164169
const subscriptionService = testInjector.resolve<SubscriptionServiceTester>(SubscriptionServiceTester);

0 commit comments

Comments
 (0)