Skip to content

Commit effd230

Browse files
Improve analytics
We have a requirement to track information in several different providers. However, this may slow down CLI's operations as each track (at least for some of the providers) may last around 100-300ms. In order to track in multiple analytics provider without slowing down CLI commands, move the tracking to a new process. The process is detached from CLI, so when CLI finishes its work it will exit, while the child process will remain alive until it sends all required information. The new process is called `analytics-broker-process`, it is detached from CLI, but it there's IPC channel between them, so CLI can send information to be tracked. Just before CLI dies, it will send "finish" message to the `broker` process and the IPC channel will be closed. In CLI implement a new `analytics-service`, so when the methods `track`, `trackFeature` and `trackException` are called, instead of tracking directly in CLI's process (i.e. the old implementation), to send information to the broker process. The only exception is tracking of the answer if user allows us to send anonymous statistics. As this action requires user interaction, it is executed in CLI's process, a new eqatec analytics monitor is started and the answer is sent to it. After that the analytics is stopped. In case the answer is "yes" and there is more information that we want to track in the current process, it will be send to broker and tracked in all providers. In case the answer is "no", no other data will be tracked. The broker process is started from analytics process once per process, i.e. on each CLI process (which have something to track) there's a corresponding `analytics-broker-process`. Once it is initialized, it sends BrokerReadyToReceive message to CLI and CLI knows that it can start sending tracking information. Initialization phase is creating a new instance of AnalyticsBroker and attaching handlers for `message` and `disconnect` events. The AnalyitcsBroker itself has a single purpose - sends tracking data to all available prociders. Currently the only provider is `eqatecAnalyticsProvider`. Once CLI sends "finish" message (or `analytics-broker-process` receives `disconnect` event), the process sends information to all providers that there's nothing more to be tracked. When all providers finish their work, they should disocnnect themselves from the broker process and it will exit gracefully. `EqatecAnalyticsProvider` is used to track features and exceptions in Progress Telerik Analytics. Our requirement is to track features in two projects (same data should be send to both of them) and exceptions in another project. Each of them should be in a separate child process, so failure in one of them will not reflect the others. So the provider starts all required processes. When broker sends information to the provider, it sends it to all required child processes, for example if a feature should be tracked, the provider will send information for it to the two processes used for feature tracking. Each eqatec child process is a new Node.js process runnign `eqatec-analytics-process`. Again - this is a detached process. Once it is initialized, it sends "EqatecAnalyticsReadyToReceive" message to the `EqatecAnalyitcsProvider` and it knows it can send information to the new child process. The initialization phase is creation of new instance of `EqatectAnalyticsService`, attaching to "message" and "disconnect" events. Once `EqatecAnalyitcsProvider` receives `EqatecAnalyticsReadyToReceive` message, it sends initialization data for the eqatec monitor. It includes the analytics project API key, user id, current sessions count, etc. The information is persisted in a file on user's machine, so this file should be updated from a single process. As we might have 3 eqatec analytics processes, the provider is the only place where the file should be read and updated. When `eqatec-analytics-process` receives initialization data, it creates a new instance of the `EqatectAnalyticsService` class with it. However a monitor is not started yet. It will be started when there's something to be tracked, i.e. once `eqatec-analytics-process` receives feature or exception to be tracked. When `eqatec-analytics-process` receives "finish" message, it awaits all pending requests and after that disconnets itself from the parent. In case this operation fails, the process just exits, which will allow the parent to exit gracefully. Remove the public `startEqatecMonitor` method from API - it has not been used.
1 parent 608737f commit effd230

16 files changed

+650
-64
lines changed

.vscode/launch.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,19 @@
6767
{
6868
"type": "node",
6969
"request": "attach",
70-
"name": "Attach to Process",
71-
"port": 5858,
70+
"name": "Attach to Broker Process",
71+
// In case you want to debug Analytics Broker process, add `--debug-brk=9897` (or --inspect-brk=9897) when spawning analytics-broker-process.
72+
"port": 9897,
73+
"sourceMaps": true
74+
},
75+
76+
{
77+
"type": "node",
78+
"request": "attach",
79+
"name": "Attach to Eqatec Process",
80+
// In case you want to debug Eqatec Analytics process, add `--debug-brk=9855` (or --inspect-brk=9855) when spawning eqatec-analytics-process.
81+
// NOTE: Ensure you set it only for one of the analytics processes.
82+
"port": 9855,
7283
"sourceMaps": true
7384
}
7485

PublicAPI.md

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -390,21 +390,6 @@ tns.npm.view(["nativescript"], {}).then(result => {
390390
});
391391
```
392392
393-
## analyticsService
394-
Provides a way to configure analytics.
395-
396-
### startEqatecMonitor
397-
* Definition:
398-
```TypeScript
399-
/**
400-
* Starts analytics monitor with provided key.
401-
* @param {string} projectApiKey API key with which to start analytics monitor.
402-
* @returns {Promise<void>}.
403-
*/
404-
startEqatecMonitor(projectApiKey: string): Promise<void>;
405-
```
406-
407-
408393
## debugService
409394
Provides methods for debugging applications on devices. The service is also event emitter, that raises the following events:
410395
* `connectionError` event - this event is raised when the debug operation cannot start on iOS device. The causes can be:

lib/bootstrap.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ $injector.require("androidDebugService", "./services/android-debug-service");
3030

3131
$injector.require("userSettingsService", "./services/user-settings-service");
3232
$injector.require("analyticsSettingsService", "./services/analytics-settings-service");
33-
$injector.requirePublic("analyticsService", "./services/analytics-service");
33+
$injector.requirePublic("analyticsService", "./services/analytics/analytics-service");
34+
$injector.require("eqatecAnalyticsProvider", "./services/analytics/eqatec-analytics-provider");
3435

3536
$injector.require("emulatorSettingsService", "./services/emulator-settings-service");
3637

lib/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class StaticConfig extends StaticConfigBase implements IStaticConfig {
7373
}
7474

7575
public get PATH_TO_BOOTSTRAP(): string {
76-
return path.join(__dirname, "bootstrap");
76+
return path.join(__dirname, "bootstrap.js");
7777
}
7878

7979
public async getAdbFilePath(): Promise<string> {

lib/services/analytics-service.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.

lib/services/analytics-settings-service.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { createGUID } from "../common/helpers";
22

33
class AnalyticsSettingsService implements IAnalyticsSettingsService {
4-
private static SESSIONS_STARTED_OBSOLETE_KEY = "SESSIONS_STARTED";
54
private static SESSIONS_STARTED_KEY_PREFIX = "SESSIONS_STARTED_";
65

76
constructor(private $userSettingsService: UserSettings.IUserSettingsService,
@@ -33,14 +32,8 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService {
3332
}
3433

3534
public async getUserSessionsCount(projectName: string): Promise<number> {
36-
const oldSessionCount = await this.$userSettingsService.getSettingValue<number>(AnalyticsSettingsService.SESSIONS_STARTED_OBSOLETE_KEY);
37-
38-
if (oldSessionCount) {
39-
// remove the old property for sessions count
40-
await this.$userSettingsService.removeSetting(AnalyticsSettingsService.SESSIONS_STARTED_OBSOLETE_KEY);
41-
}
42-
43-
return await this.$userSettingsService.getSettingValue<number>(this.getSessionsProjectKey(projectName)) || oldSessionCount || 0;
35+
const sessionsCountForProject = await this.$userSettingsService.getSettingValue<number>(this.getSessionsProjectKey(projectName));
36+
return sessionsCountForProject || 0;
4437
}
4538

4639
public async setUserSessionsCount(count: number, projectName: string): Promise<void> {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as fs from "fs";
2+
import { AnalyticsBroker } from "./analytics-broker";
3+
4+
const pathToBootstrap = process.argv[2];
5+
if (!pathToBootstrap || !fs.existsSync(pathToBootstrap)) {
6+
throw new Error("Invalid path to bootstrap.");
7+
}
8+
9+
// After requiring the bootstrap we can use $injector
10+
require(pathToBootstrap);
11+
12+
const analyticsBroker = $injector.resolve<IAnalyticsBroker>(AnalyticsBroker, { pathToBootstrap });
13+
let trackingQueue: Promise<void> = Promise.resolve();
14+
15+
let sentFinishMsg = false;
16+
let receivedFinishMsg = false;
17+
18+
const sendDataForTracking = async (data: ITrackingInformation) => {
19+
trackingQueue = trackingQueue.then(() => analyticsBroker.sendDataForTracking(data));
20+
await trackingQueue;
21+
};
22+
23+
const finishTracking = async (data?: ITrackingInformation) => {
24+
if (!sentFinishMsg) {
25+
sentFinishMsg = true;
26+
27+
data = data || { type: TrackingTypes.Finish };
28+
29+
if (receivedFinishMsg) {
30+
await sendDataForTracking(data);
31+
} else {
32+
// In case we've got here without receiving "finish" message from parent (receivedFinishMsg is false)
33+
// there might be various reasons, but most probably the parent is dead.
34+
// However, there's no guarantee that we've received all messages. So wait some time before sending finish message to children.
35+
setTimeout(async () => {
36+
await sendDataForTracking(data);
37+
}, 1000);
38+
}
39+
}
40+
};
41+
42+
process.on("message", async (data: ITrackingInformation) => {
43+
if (data.type === TrackingTypes.Finish) {
44+
receivedFinishMsg = true;
45+
await finishTracking(data);
46+
return;
47+
}
48+
49+
await sendDataForTracking(data);
50+
});
51+
52+
process.on("disconnect", async () => {
53+
await finishTracking();
54+
});
55+
56+
process.send(AnalyticsMessages.BrokerReadyToReceive);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { cache } from "../../common/decorators";
2+
3+
export class AnalyticsBroker implements IAnalyticsBroker {
4+
5+
@cache()
6+
private get $eqatecAnalyticsProvider(): IAnalyticsProvider {
7+
return this.$injector.resolve("eqatecAnalyticsProvider", { pathToBootstrap: this.pathToBootstrap });
8+
}
9+
10+
constructor(private pathToBootstrap: string,
11+
private $injector: IInjector) { }
12+
13+
private get analyticsProviders(): IAnalyticsProvider[] {
14+
return [
15+
this.$eqatecAnalyticsProvider
16+
];
17+
}
18+
19+
public async sendDataForTracking(trackInfo: ITrackingInformation): Promise<void> {
20+
for (const provider of this.analyticsProviders) {
21+
switch (trackInfo.type) {
22+
case TrackingTypes.Exception:
23+
await provider.trackException(<IExceptionsTrackingInformation>trackInfo);
24+
break;
25+
case TrackingTypes.Feature:
26+
await provider.trackFeature(<IFeatureTrackingInformation>trackInfo);
27+
break;
28+
case TrackingTypes.Finish:
29+
await provider.finishTracking();
30+
break;
31+
default:
32+
throw new Error(`Invalid tracking type: ${trackInfo.type}`);
33+
}
34+
35+
}
36+
37+
}
38+
39+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Defines messages used in communication between CLI's process and analytics subprocesses.
3+
*/
4+
const enum AnalyticsMessages {
5+
/**
6+
* Analytics Broker is initialized and is ready to receive information for tracking.
7+
*/
8+
BrokerReadyToReceive = "BrokerReadyToReceive",
9+
10+
/**
11+
* Eqatec Analytics process is initialized and is ready to receive information for tracking.
12+
*/
13+
EqatecAnalyticsReadyToReceive = "EqatecAnalyticsReadyToReceive"
14+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { AnalyticsServiceBase } from "../../common/services/analytics-service-base";
2+
import { ChildProcess } from "child_process";
3+
import * as path from "path";
4+
import * as helpers from "../../common/helpers";
5+
import { cache } from "../../common/decorators";
6+
7+
export class AnalyticsService extends AnalyticsServiceBase implements IAnalyticsService {
8+
private static ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY = "9912cff308334c6d9ad9c33f76a983e3";
9+
private static ANALYTICS_BROKER_START_TIMEOUT = 30 * 1000;
10+
11+
constructor(protected $logger: ILogger,
12+
protected $options: IOptions,
13+
$staticConfig: Config.IStaticConfig,
14+
$prompter: IPrompter,
15+
$userSettingsService: UserSettings.IUserSettingsService,
16+
$analyticsSettingsService: IAnalyticsSettingsService,
17+
$progressIndicator: IProgressIndicator,
18+
$osInfo: IOsInfo,
19+
private $childProcess: IChildProcess,
20+
private $processService: IProcessService) {
21+
super($logger, $options, $staticConfig, $prompter, $userSettingsService, $analyticsSettingsService, $progressIndicator, $osInfo);
22+
}
23+
24+
public async track(featureName: string, featureValue: string): Promise<void> {
25+
await this.sendDataForTracking(featureName, featureValue);
26+
}
27+
28+
public async trackException(exception: any, message: string): Promise<void> {
29+
await this.sendExceptionForTracking(exception, message);
30+
}
31+
32+
public async trackFeature(featureValue: string): Promise<void> {
33+
const featureName = this.$options.analyticsClient ||
34+
(helpers.isInteractive() ? "CLI" : "Non-interactive");
35+
36+
await this.sendDataForTracking(featureName, featureValue);
37+
}
38+
39+
protected async checkConsentCore(trackFeatureUsage: boolean): Promise<void> {
40+
await this.restartEqatecMonitor(AnalyticsService.ANALYTICS_FEATURE_USAGE_TRACKING_API_KEY);
41+
await super.checkConsentCore(trackFeatureUsage);
42+
43+
// Stop the monitor, so correct API_KEY will be used when features are tracked.
44+
this.tryStopEqatecMonitor();
45+
}
46+
47+
@cache()
48+
private getAnalyticsBroker(): Promise<ChildProcess> {
49+
return new Promise<ChildProcess>((resolve, reject) => {
50+
const broker = this.$childProcess.spawn("node",
51+
[
52+
path.join(__dirname, "analytics-broker-process.js"),
53+
this.$staticConfig.PATH_TO_BOOTSTRAP
54+
],
55+
{
56+
stdio: ["ignore", "ignore", "ignore", "ipc"],
57+
detached: true
58+
}
59+
);
60+
61+
broker.unref();
62+
63+
let isSettled = false;
64+
65+
const timeoutId = setTimeout(() => {
66+
if (!isSettled) {
67+
reject(new Error("Unable to start Analytics Broker process."));
68+
}
69+
}, AnalyticsService.ANALYTICS_BROKER_START_TIMEOUT);
70+
71+
broker.on("error", (err: Error) => {
72+
clearTimeout(timeoutId);
73+
74+
if (!isSettled) {
75+
isSettled = true;
76+
reject(err);
77+
}
78+
});
79+
80+
broker.on("message", (data: any) => {
81+
if (data === AnalyticsMessages.BrokerReadyToReceive) {
82+
clearTimeout(timeoutId);
83+
84+
if (!isSettled) {
85+
isSettled = true;
86+
resolve(broker);
87+
}
88+
}
89+
});
90+
91+
this.$processService.attachToProcessExitSignals(this, () => {
92+
broker.send({
93+
type: TrackingTypes.Finish
94+
});
95+
});
96+
97+
});
98+
99+
}
100+
101+
private async sendDataForTracking(featureName: string, featureValue: string): Promise<void> {
102+
await this.initAnalyticsStatuses();
103+
104+
if (this.analyticsStatuses[this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME] === AnalyticsStatus.enabled) {
105+
const broker = await this.getAnalyticsBroker();
106+
107+
return new Promise<void>((resolve, reject) => {
108+
broker.send(
109+
{
110+
type: TrackingTypes.Feature,
111+
featureName: featureName,
112+
featureValue: featureValue
113+
},
114+
() => {
115+
resolve();
116+
});
117+
});
118+
119+
}
120+
121+
}
122+
123+
private async sendExceptionForTracking(exception: Error, message: string): Promise<void> {
124+
await this.initAnalyticsStatuses();
125+
126+
if (this.analyticsStatuses[this.$staticConfig.ERROR_REPORT_SETTING_NAME] === AnalyticsStatus.enabled) {
127+
const broker = await this.getAnalyticsBroker();
128+
129+
return new Promise<void>((resolve, reject) => {
130+
broker.send(
131+
{
132+
type: TrackingTypes.Exception,
133+
exception,
134+
message
135+
},
136+
() => {
137+
resolve();
138+
});
139+
140+
});
141+
142+
}
143+
144+
}
145+
146+
}
147+
148+
$injector.register("analyticsService", AnalyticsService);

0 commit comments

Comments
 (0)