Skip to content

Commit c7efa01

Browse files
feat: open feedback form when CLI is uninstalled
In case CLI is uninstalled, open a feedback form, so the user might explain why they've uninstalled the product. Add analytics tracking for the uninstall call. Remove the `disableAnalytics` property of the `dev-preuninstall` command - this way we'll receive information for the number of times the command has been executed (usually it is called from CLI's preuninstall script). Also add custom data in the analytics, so we'll be able to generate report to see how many times users uninstall the CLI (actually executing `npm uninstall...`) and if this happens from interactive terminal.
1 parent 2fc81c5 commit c7efa01

File tree

3 files changed

+168
-6
lines changed

3 files changed

+168
-6
lines changed

lib/common/commands/preuninstall.ts

+21-5
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,44 @@
11
import * as path from "path";
2-
import { doesCurrentNpmCommandMatch } from "../helpers";
2+
import { doesCurrentNpmCommandMatch, isInteractive } from "../helpers";
3+
import { TrackActionNames, AnalyticsEventLabelDelimiter } from "../../constants";
34

45
export class PreUninstallCommand implements ICommand {
5-
public disableAnalytics = true;
6+
private static FEEDBACK_FORM_URL = "https://www.nativescript.org/uninstall-feedback";
67

78
public allowedParameters: ICommandParameter[] = [];
89

9-
constructor(private $extensibilityService: IExtensibilityService,
10+
constructor(private $analyticsService: IAnalyticsService,
11+
private $extensibilityService: IExtensibilityService,
1012
private $fs: IFileSystem,
13+
private $opener: IOpener,
1114
private $packageInstallationManager: IPackageInstallationManager,
1215
private $settingsService: ISettingsService) { }
1316

1417
public async execute(args: string[]): Promise<void> {
1518
const isIntentionalUninstall = doesCurrentNpmCommandMatch([/^uninstall$/, /^remove$/, /^rm$/, /^r$/, /^un$/, /^unlink$/]);
19+
20+
await this.$analyticsService.trackEventActionInGoogleAnalytics({
21+
action: TrackActionNames.UninstallCLI,
22+
additionalData: `isIntentionalUninstall${AnalyticsEventLabelDelimiter}${isIntentionalUninstall}${AnalyticsEventLabelDelimiter}isInteractive${AnalyticsEventLabelDelimiter}${!!isInteractive()}`
23+
});
24+
1625
if (isIntentionalUninstall) {
17-
this.handleIntentionalUninstall();
26+
await this.handleIntentionalUninstall();
1827
}
1928

2029
this.$fs.deleteFile(path.join(this.$settingsService.getProfileDir(), "KillSwitches", "cli"));
2130
}
2231

23-
private handleIntentionalUninstall(): void {
32+
private async handleIntentionalUninstall(): Promise<void> {
2433
this.$extensibilityService.removeAllExtensions();
2534
this.$packageInstallationManager.clearInspectorCache();
35+
await this.handleFeedbackForm();
36+
}
37+
38+
private async handleFeedbackForm(): Promise<void> {
39+
if (isInteractive()) {
40+
this.$opener.open(PreUninstallCommand.FEEDBACK_FORM_URL);
41+
}
2642
}
2743
}
2844

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { assert } from "chai";
2+
import { Yok } from "../../yok";
3+
import { PreUninstallCommand } from "../../commands/preuninstall";
4+
import * as path from "path";
5+
const helpers = require("../../helpers");
6+
7+
describe("preuninstall", () => {
8+
const profileDir = "profileDir";
9+
const createTestInjector = (): IInjector => {
10+
const testInjector = new Yok();
11+
12+
testInjector.register("extensibilityService", {
13+
removeAllExtensions: (): void => undefined
14+
});
15+
16+
testInjector.register("fs", {
17+
deleteFile: (pathToFile: string): void => undefined
18+
});
19+
20+
testInjector.register("packageInstallationManager", {
21+
clearInspectorCache: (): void => undefined
22+
});
23+
24+
testInjector.register("settingsService", {
25+
getProfileDir: (): string => profileDir
26+
});
27+
28+
testInjector.register("opener", {
29+
open: (filename: string, appname?: string): void => undefined
30+
});
31+
32+
testInjector.register("analyticsService", {
33+
trackEventActionInGoogleAnalytics: async (data: IEventActionData): Promise<void> => undefined
34+
});
35+
36+
testInjector.registerCommand("dev-preuninstall", PreUninstallCommand);
37+
38+
return testInjector;
39+
};
40+
41+
it("cleans the KillSwitches/cli file", async () => {
42+
helpers.doesCurrentNpmCommandMatch = () => false;
43+
const testInjector = createTestInjector();
44+
const fs = testInjector.resolve<IFileSystem>("fs");
45+
const deletedFiles: string[] = [];
46+
fs.deleteFile = (pathToFile: string) => {
47+
deletedFiles.push(pathToFile);
48+
};
49+
50+
const preUninstallCommand: ICommand = testInjector.resolveCommand("dev-preuninstall");
51+
await preUninstallCommand.execute([]);
52+
assert.deepEqual(deletedFiles, [path.join(profileDir, "KillSwitches", "cli")]);
53+
});
54+
55+
it("tracks correct data in analytics", async () => {
56+
const testData: { isInteractive: boolean, isIntentionalUninstall: boolean, expecteEventLabelData: string }[] = [
57+
{
58+
isIntentionalUninstall: false,
59+
isInteractive: false,
60+
expecteEventLabelData: `isIntentionalUninstall__false__isInteractive__false`
61+
},
62+
{
63+
isIntentionalUninstall: true,
64+
isInteractive: false,
65+
expecteEventLabelData: `isIntentionalUninstall__true__isInteractive__false`
66+
},
67+
{
68+
isIntentionalUninstall: false,
69+
isInteractive: true,
70+
expecteEventLabelData: `isIntentionalUninstall__false__isInteractive__true`
71+
},
72+
{
73+
isIntentionalUninstall: true,
74+
isInteractive: true,
75+
expecteEventLabelData: `isIntentionalUninstall__true__isInteractive__true`
76+
}
77+
];
78+
79+
const testInjector = createTestInjector();
80+
const analyticsService = testInjector.resolve<IAnalyticsService>("analyticsService");
81+
let trackedData: IEventActionData[] = [];
82+
analyticsService.trackEventActionInGoogleAnalytics = async (data: IEventActionData): Promise<void> => {
83+
trackedData.push(data);
84+
};
85+
86+
const preUninstallCommand: ICommand = testInjector.resolveCommand("dev-preuninstall");
87+
for (const testCase of testData) {
88+
helpers.isInteractive = () => testCase.isInteractive;
89+
helpers.doesCurrentNpmCommandMatch = () => testCase.isIntentionalUninstall;
90+
await preUninstallCommand.execute([]);
91+
assert.deepEqual(trackedData, [{
92+
action: "Uninstall CLI",
93+
additionalData: testCase.expecteEventLabelData
94+
}]);
95+
trackedData = [];
96+
}
97+
});
98+
99+
it("removes all extensions and inspector cache when uninstall command is executed", async () => {
100+
helpers.doesCurrentNpmCommandMatch = () => true;
101+
helpers.isInteractive = () => false;
102+
103+
const testInjector = createTestInjector();
104+
const fs = testInjector.resolve<IFileSystem>("fs");
105+
const deletedFiles: string[] = [];
106+
fs.deleteFile = (pathToFile: string) => {
107+
deletedFiles.push(pathToFile);
108+
};
109+
110+
const extensibilityService = testInjector.resolve<IExtensibilityService>("extensibilityService");
111+
let isRemoveAllExtensionsCalled = false;
112+
extensibilityService.removeAllExtensions = () => {
113+
isRemoveAllExtensionsCalled = true;
114+
};
115+
116+
const packageInstallationManager = testInjector.resolve<IPackageInstallationManager>("packageInstallationManager");
117+
let isClearInspectorCacheCalled = false;
118+
packageInstallationManager.clearInspectorCache = () => {
119+
isClearInspectorCacheCalled = true;
120+
};
121+
122+
const preUninstallCommand: ICommand = testInjector.resolveCommand("dev-preuninstall");
123+
await preUninstallCommand.execute([]);
124+
assert.deepEqual(deletedFiles, [path.join(profileDir, "KillSwitches", "cli")]);
125+
126+
assert.isTrue(isRemoveAllExtensionsCalled, "When uninstall is called, `removeAllExtensions` method must be called");
127+
assert.isTrue(isClearInspectorCacheCalled, "When uninstall is called, `clearInspectorCache` method must be called");
128+
});
129+
130+
it("opens the uninstall feedback form when terminal is interactive and uninstall is called", async () => {
131+
helpers.doesCurrentNpmCommandMatch = () => true;
132+
helpers.isInteractive = () => true;
133+
134+
const testInjector = createTestInjector();
135+
const opener = testInjector.resolve<IOpener>("opener");
136+
const openParams: any[] = [];
137+
opener.open = (filename: string, appname?: string) => {
138+
openParams.push({ filename, appname });
139+
};
140+
141+
const preUninstallCommand: ICommand = testInjector.resolveCommand("dev-preuninstall");
142+
await preUninstallCommand.execute([]);
143+
assert.deepEqual(openParams, [{ filename: "https://www.nativescript.org/uninstall-feedback", appname: undefined }]);
144+
});
145+
});

lib/constants.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,8 @@ export const enum TrackActionNames {
185185
Options = "Options",
186186
AcceptTracking = "Accept Tracking",
187187
Performance = "Performance",
188-
PreviewAppData = "Preview App Data"
188+
PreviewAppData = "Preview App Data",
189+
UninstallCLI = "Uninstall CLI"
189190
}
190191

191192
export const AnalyticsEventLabelDelimiter = "__";

0 commit comments

Comments
 (0)