From c7efa0120a8bf469c0c1f6499317baa57bf1dde9 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Sun, 25 Aug 2019 22:59:46 +0300 Subject: [PATCH] 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. --- lib/common/commands/preuninstall.ts | 26 +++- lib/common/test/unit-tests/preuninstall.ts | 145 +++++++++++++++++++++ lib/constants.ts | 3 +- 3 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 lib/common/test/unit-tests/preuninstall.ts diff --git a/lib/common/commands/preuninstall.ts b/lib/common/commands/preuninstall.ts index 41ba5c3c4e..a808f41513 100644 --- a/lib/common/commands/preuninstall.ts +++ b/lib/common/commands/preuninstall.ts @@ -1,28 +1,44 @@ import * as path from "path"; -import { doesCurrentNpmCommandMatch } from "../helpers"; +import { doesCurrentNpmCommandMatch, isInteractive } from "../helpers"; +import { TrackActionNames, AnalyticsEventLabelDelimiter } from "../../constants"; export class PreUninstallCommand implements ICommand { - public disableAnalytics = true; + private static FEEDBACK_FORM_URL = "https://www.nativescript.org/uninstall-feedback"; public allowedParameters: ICommandParameter[] = []; - constructor(private $extensibilityService: IExtensibilityService, + constructor(private $analyticsService: IAnalyticsService, + private $extensibilityService: IExtensibilityService, private $fs: IFileSystem, + private $opener: IOpener, private $packageInstallationManager: IPackageInstallationManager, private $settingsService: ISettingsService) { } public async execute(args: string[]): Promise { const isIntentionalUninstall = doesCurrentNpmCommandMatch([/^uninstall$/, /^remove$/, /^rm$/, /^r$/, /^un$/, /^unlink$/]); + + await this.$analyticsService.trackEventActionInGoogleAnalytics({ + action: TrackActionNames.UninstallCLI, + additionalData: `isIntentionalUninstall${AnalyticsEventLabelDelimiter}${isIntentionalUninstall}${AnalyticsEventLabelDelimiter}isInteractive${AnalyticsEventLabelDelimiter}${!!isInteractive()}` + }); + if (isIntentionalUninstall) { - this.handleIntentionalUninstall(); + await this.handleIntentionalUninstall(); } this.$fs.deleteFile(path.join(this.$settingsService.getProfileDir(), "KillSwitches", "cli")); } - private handleIntentionalUninstall(): void { + private async handleIntentionalUninstall(): Promise { this.$extensibilityService.removeAllExtensions(); this.$packageInstallationManager.clearInspectorCache(); + await this.handleFeedbackForm(); + } + + private async handleFeedbackForm(): Promise { + if (isInteractive()) { + this.$opener.open(PreUninstallCommand.FEEDBACK_FORM_URL); + } } } diff --git a/lib/common/test/unit-tests/preuninstall.ts b/lib/common/test/unit-tests/preuninstall.ts new file mode 100644 index 0000000000..2f2ae3a978 --- /dev/null +++ b/lib/common/test/unit-tests/preuninstall.ts @@ -0,0 +1,145 @@ +import { assert } from "chai"; +import { Yok } from "../../yok"; +import { PreUninstallCommand } from "../../commands/preuninstall"; +import * as path from "path"; +const helpers = require("../../helpers"); + +describe("preuninstall", () => { + const profileDir = "profileDir"; + const createTestInjector = (): IInjector => { + const testInjector = new Yok(); + + testInjector.register("extensibilityService", { + removeAllExtensions: (): void => undefined + }); + + testInjector.register("fs", { + deleteFile: (pathToFile: string): void => undefined + }); + + testInjector.register("packageInstallationManager", { + clearInspectorCache: (): void => undefined + }); + + testInjector.register("settingsService", { + getProfileDir: (): string => profileDir + }); + + testInjector.register("opener", { + open: (filename: string, appname?: string): void => undefined + }); + + testInjector.register("analyticsService", { + trackEventActionInGoogleAnalytics: async (data: IEventActionData): Promise => undefined + }); + + testInjector.registerCommand("dev-preuninstall", PreUninstallCommand); + + return testInjector; + }; + + it("cleans the KillSwitches/cli file", async () => { + helpers.doesCurrentNpmCommandMatch = () => false; + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + const deletedFiles: string[] = []; + fs.deleteFile = (pathToFile: string) => { + deletedFiles.push(pathToFile); + }; + + const preUninstallCommand: ICommand = testInjector.resolveCommand("dev-preuninstall"); + await preUninstallCommand.execute([]); + assert.deepEqual(deletedFiles, [path.join(profileDir, "KillSwitches", "cli")]); + }); + + it("tracks correct data in analytics", async () => { + const testData: { isInteractive: boolean, isIntentionalUninstall: boolean, expecteEventLabelData: string }[] = [ + { + isIntentionalUninstall: false, + isInteractive: false, + expecteEventLabelData: `isIntentionalUninstall__false__isInteractive__false` + }, + { + isIntentionalUninstall: true, + isInteractive: false, + expecteEventLabelData: `isIntentionalUninstall__true__isInteractive__false` + }, + { + isIntentionalUninstall: false, + isInteractive: true, + expecteEventLabelData: `isIntentionalUninstall__false__isInteractive__true` + }, + { + isIntentionalUninstall: true, + isInteractive: true, + expecteEventLabelData: `isIntentionalUninstall__true__isInteractive__true` + } + ]; + + const testInjector = createTestInjector(); + const analyticsService = testInjector.resolve("analyticsService"); + let trackedData: IEventActionData[] = []; + analyticsService.trackEventActionInGoogleAnalytics = async (data: IEventActionData): Promise => { + trackedData.push(data); + }; + + const preUninstallCommand: ICommand = testInjector.resolveCommand("dev-preuninstall"); + for (const testCase of testData) { + helpers.isInteractive = () => testCase.isInteractive; + helpers.doesCurrentNpmCommandMatch = () => testCase.isIntentionalUninstall; + await preUninstallCommand.execute([]); + assert.deepEqual(trackedData, [{ + action: "Uninstall CLI", + additionalData: testCase.expecteEventLabelData + }]); + trackedData = []; + } + }); + + it("removes all extensions and inspector cache when uninstall command is executed", async () => { + helpers.doesCurrentNpmCommandMatch = () => true; + helpers.isInteractive = () => false; + + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + const deletedFiles: string[] = []; + fs.deleteFile = (pathToFile: string) => { + deletedFiles.push(pathToFile); + }; + + const extensibilityService = testInjector.resolve("extensibilityService"); + let isRemoveAllExtensionsCalled = false; + extensibilityService.removeAllExtensions = () => { + isRemoveAllExtensionsCalled = true; + }; + + const packageInstallationManager = testInjector.resolve("packageInstallationManager"); + let isClearInspectorCacheCalled = false; + packageInstallationManager.clearInspectorCache = () => { + isClearInspectorCacheCalled = true; + }; + + const preUninstallCommand: ICommand = testInjector.resolveCommand("dev-preuninstall"); + await preUninstallCommand.execute([]); + assert.deepEqual(deletedFiles, [path.join(profileDir, "KillSwitches", "cli")]); + + assert.isTrue(isRemoveAllExtensionsCalled, "When uninstall is called, `removeAllExtensions` method must be called"); + assert.isTrue(isClearInspectorCacheCalled, "When uninstall is called, `clearInspectorCache` method must be called"); + }); + + it("opens the uninstall feedback form when terminal is interactive and uninstall is called", async () => { + helpers.doesCurrentNpmCommandMatch = () => true; + helpers.isInteractive = () => true; + + const testInjector = createTestInjector(); + const opener = testInjector.resolve("opener"); + const openParams: any[] = []; + opener.open = (filename: string, appname?: string) => { + openParams.push({ filename, appname }); + }; + + const preUninstallCommand: ICommand = testInjector.resolveCommand("dev-preuninstall"); + await preUninstallCommand.execute([]); + assert.deepEqual(openParams, [{ filename: "https://www.nativescript.org/uninstall-feedback", appname: undefined }]); + }); +}); diff --git a/lib/constants.ts b/lib/constants.ts index 93c3516fca..fd8f561e7f 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -185,7 +185,8 @@ export const enum TrackActionNames { Options = "Options", AcceptTracking = "Accept Tracking", Performance = "Performance", - PreviewAppData = "Preview App Data" + PreviewAppData = "Preview App Data", + UninstallCLI = "Uninstall CLI" } export const AnalyticsEventLabelDelimiter = "__";