Skip to content

Commit f2bffae

Browse files
Merge pull request #4989 from NativeScript/vladimirov/update-feedback-analytics
fix: ensure data is sent to analytics on preuninstall
2 parents f7eaafa + bab894d commit f2bffae

File tree

8 files changed

+94
-6
lines changed

8 files changed

+94
-6
lines changed

lib/common/commands/preuninstall.ts

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class PreUninstallCommand implements ICommand {
2727
}
2828

2929
this.$fs.deleteFile(path.join(this.$settingsService.getProfileDir(), "KillSwitches", "cli"));
30+
await this.$analyticsService.finishTracking();
3031
}
3132

3233
private async handleIntentionalUninstall(): Promise<void> {

lib/common/declarations.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ declare const enum TrackingTypes {
207207
* Defines that the broker process should get and track the data from preview app to Google Analytics
208208
*/
209209
PreviewAppData = "PreviewAppData",
210+
211+
/**
212+
* Defines that the broker process should send all the pending information to Analytics.
213+
* After that the process should send information it has finished tracking and die gracefully.
214+
*/
215+
FinishTracking = "FinishTracking",
210216
}
211217

212218
/**
@@ -688,6 +694,7 @@ interface IAnalyticsService {
688694
setStatus(settingName: string, enabled: boolean): Promise<void>;
689695
getStatusMessage(settingName: string, jsonFormat: boolean, readableSettingName: string): Promise<string>;
690696
isEnabled(settingName: string): Promise<boolean>;
697+
finishTracking(): Promise<void>;
691698

692699
/**
693700
* Tracks the answer of question if user allows to be tracked.

lib/common/test/unit-tests/preuninstall.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ describe("preuninstall", () => {
3030
});
3131

3232
testInjector.register("analyticsService", {
33-
trackEventActionInGoogleAnalytics: async (data: IEventActionData): Promise<void> => undefined
33+
trackEventActionInGoogleAnalytics: async (data: IEventActionData): Promise<void> => undefined,
34+
finishTracking: async(): Promise<void> => undefined
3435
});
3536

3637
testInjector.registerCommand("dev-preuninstall", PreUninstallCommand);
@@ -83,15 +84,22 @@ describe("preuninstall", () => {
8384
trackedData.push(data);
8485
};
8586

87+
let isFinishTrackingCalled = false;
88+
analyticsService.finishTracking = async (): Promise<void> => {
89+
isFinishTrackingCalled = true;
90+
};
91+
8692
const preUninstallCommand: ICommand = testInjector.resolveCommand("dev-preuninstall");
8793
for (const testCase of testData) {
8894
helpers.isInteractive = () => testCase.isInteractive;
8995
helpers.doesCurrentNpmCommandMatch = () => testCase.isIntentionalUninstall;
96+
isFinishTrackingCalled = false;
9097
await preUninstallCommand.execute([]);
9198
assert.deepEqual(trackedData, [{
9299
action: "Uninstall CLI",
93100
additionalData: testCase.expecteEventLabelData
94101
}]);
102+
assert.isTrue(isFinishTrackingCalled, "At the end of the command, finishTracking must be called");
95103
trackedData = [];
96104
}
97105
});

lib/detached-processes/detached-process-enums.d.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ declare const enum DetachedProcessMessages {
55
/**
66
* The detached process is initialized and is ready to receive information for tracking.
77
*/
8-
ProcessReadyToReceive = "ProcessReadyToReceive"
8+
ProcessReadyToReceive = "ProcessReadyToReceive",
9+
10+
/**
11+
* The detached process finished its tasks and will now exit.
12+
*/
13+
ProcessFinishedTasks = "ProcessFinishedTasks"
914
}
1015

1116
/**

lib/services/analytics/analytics-broker-process.ts

+19
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ const finishTracking = async (data?: ITrackingInformation) => {
2929
analyticsLoggingService.logData({ message: `analytics-broker-process finish tracking started` });
3030
await trackingQueue;
3131
analyticsLoggingService.logData({ message: `analytics-broker-process tracking finished` });
32+
33+
};
34+
35+
const killCurrentProcessGracefully = () => {
3236
$injector.dispose();
3337
process.exit();
3438
};
@@ -68,12 +72,27 @@ process.on("message", async (data: ITrackingInformation) => {
6872
return;
6973
}
7074

75+
if (data.type === TrackingTypes.FinishTracking) {
76+
await finishTracking();
77+
78+
if (process.connected) {
79+
analyticsLoggingService.logData({ message: `analytics-broker-process will send ${DetachedProcessMessages.ProcessFinishedTasks} message` });
80+
process.send(DetachedProcessMessages.ProcessFinishedTasks, () => {
81+
analyticsLoggingService.logData({ message: `analytics-broker-process sent ${DetachedProcessMessages.ProcessFinishedTasks} message and will exit gracefully now` });
82+
killCurrentProcessGracefully();
83+
});
84+
}
85+
86+
return;
87+
}
88+
7189
await sendDataForTracking(data);
7290
});
7391

7492
process.on("disconnect", async () => {
7593
analyticsLoggingService.logData({ message: "analytics-broker-process received process.disconnect event" });
7694
await finishTracking();
95+
killCurrentProcessGracefully();
7796
});
7897

7998
analyticsLoggingService.logData({ message: `analytics-broker-process will send ${DetachedProcessMessages.ProcessReadyToReceive} message` });

lib/services/analytics/analytics-service.ts

+28
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,34 @@ export class AnalyticsService implements IAnalyticsService, IDisposable {
146146
await this.trackInGoogleAnalytics(eventActionData);
147147
}
148148

149+
public async finishTracking(): Promise<void> {
150+
return new Promise((resolve, reject) => {
151+
if (this.brokerProcess && this.brokerProcess.connected) {
152+
let timer: NodeJS.Timer;
153+
154+
const handler = (data: string) => {
155+
if (data === DetachedProcessMessages.ProcessFinishedTasks) {
156+
this.brokerProcess.removeListener("message", handler);
157+
clearTimeout(timer);
158+
resolve();
159+
}
160+
};
161+
162+
timer = setTimeout(() => {
163+
this.brokerProcess.removeListener("message", handler);
164+
resolve();
165+
}, 3000);
166+
167+
this.brokerProcess.on("message", handler);
168+
169+
const msg = { type: TrackingTypes.FinishTracking };
170+
this.brokerProcess.send(msg, (err: Error) => this.$logger.trace(`Error while sending ${JSON.stringify(msg)}`));
171+
} else {
172+
resolve();
173+
}
174+
});
175+
}
176+
149177
private forcefullyTrackInGoogleAnalytics(gaSettings: IGoogleAnalyticsData): Promise<void> {
150178
gaSettings.customDimensions = gaSettings.customDimensions || {};
151179
gaSettings.customDimensions[GoogleAnalyticsCustomDimensions.client] = this.$options.analyticsClient || (isInteractive() ? AnalyticsClients.Cli : AnalyticsClients.Unknown);

preuninstall.js

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
var path = require("path");
44
var child_process = require("child_process");
55
var commandArgs = [path.join(__dirname, "bin", "tns"), "dev-preuninstall"];
6+
if (process.env.NS_CLI_PREUNINSTALL_ANALYTICS_LOG_FILE) {
7+
commandArgs.push("--analyticsLogFile", process.env.NS_CLI_PREUNINSTALL_ANALYTICS_LOG_FILE);
8+
}
69

710
var childProcess = child_process.spawn(process.execPath, commandArgs, { stdio: "inherit"})
811

test/preuninstall.ts

+21-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const childProcess = require("child_process");
66
import { SpawnOptions, ChildProcess } from "child_process";
77
import * as path from "path";
88
import { EventEmitter } from "events";
9+
import { readFileSync } from "fs";
910

1011
describe("preuninstall.js", () => {
1112
let isSpawnCalled = false;
@@ -46,10 +47,7 @@ describe("preuninstall.js", () => {
4647

4748
const expectedPathToCliExecutable = path.join(__dirname, "..", "bin", "tns");
4849

49-
assert.isTrue(argsPassedToSpawn.indexOf(expectedPathToCliExecutable) !== -1, `The spawned args must contain path to TNS.
50-
Expected path is: ${expectedPathToCliExecutable}, current args are: ${argsPassedToSpawn}.`);
51-
assert.isTrue(argsPassedToSpawn.indexOf("dev-preuninstall") !== -1, `The spawned args must contain the name of the preuninstall command.
52-
Expected path is: ${expectedPathToCliExecutable}, current args are: ${argsPassedToSpawn}.`);
50+
assert.deepEqual(argsPassedToSpawn, [expectedPathToCliExecutable, "dev-preuninstall"]);
5351
assert.deepEqual(optionsPassedToSpawn, [{ stdio: "inherit" }], "The stdio must be inherit as this way CLI's command can determine correctly if terminal is in interactive mode.");
5452
assert.deepEqual(dataPassedToConsoleError, []);
5553

@@ -58,6 +56,25 @@ describe("preuninstall.js", () => {
5856
assert.deepEqual(dataPassedToConsoleError, [`Failed to complete all pre-uninstall steps. Error is ${errMsg}`]);
5957
});
6058

59+
it("passes --analyticsLogFile option when NS_CLI_PREUNINSTALL_ANALYTICS_LOG_FILE is set", () => {
60+
const content = readFileSync(path.join(__dirname, "..", "preuninstall.js")).toString();
61+
const originalEnvValue = process.env.NS_CLI_PREUNINSTALL_ANALYTICS_LOG_FILE;
62+
process.env.NS_CLI_PREUNINSTALL_ANALYTICS_LOG_FILE = "value from env analyticsLog.txt";
63+
/* tslint:disable:no-eval */
64+
eval(content);
65+
/* tslint:enable:no-eval */
66+
process.env.NS_CLI_PREUNINSTALL_ANALYTICS_LOG_FILE = originalEnvValue;
67+
assert.isTrue(isSpawnCalled, "child_process.spawn must be called from preuninstall.js");
68+
69+
// NOTE: As the script is eval'd, the `__dirname` in it is resolved to current file's location,
70+
// so the expectedPathToCliExecutable should be set as it is located in current dir.
71+
const expectedPathToCliExecutable = path.join(__dirname, "bin", "tns");
72+
73+
assert.deepEqual(argsPassedToSpawn, [expectedPathToCliExecutable, "dev-preuninstall", "--analyticsLogFile", "value from env analyticsLog.txt"]);
74+
assert.deepEqual(optionsPassedToSpawn, [{ stdio: "inherit" }], "The stdio must be inherit as this way CLI's command can determine correctly if terminal is in interactive mode.");
75+
assert.deepEqual(dataPassedToConsoleError, []);
76+
});
77+
6178
it("ensure package.json has correct preuninstall script", () => {
6279
const packageJsonData = require("../package.json");
6380
assert.equal(packageJsonData.scripts.preuninstall, "node preuninstall.js");

0 commit comments

Comments
 (0)