Skip to content

Commit 2f1cfdc

Browse files
Add tracking to Google Analytics
Add new methods to track information in Google Analytics(GA). As the data that we want to track is different than the one we track in Eqatec Analytics, call the new methods whenever we want to send some information to GA. Change the broker process to send information to Eqatec Analytics when we want to track feature or exception and to GA only when the new method for tracking there is called. In Google Analytics (GA) we track two types of data - pages (called pageviews) and events. Each command is tracked as a page - we only track the name of the command as a page: * `tns build android` will be tracked as visited page - `build android` * `tns run` will be tracked as visited page - `run` When we want to track some additional actions that happen during execution of each command, we send events to GA. Each event has the following data: * category - currently we do not have a specific category for each action, so it is hardcoded to "CLI". * action - this is the real action that will be tracked, for example `Build`, `LiveSync`, `Deploy`, etc. * label - contains specific inforamation for the current action, for example what is the platform, device type, etc. In many cases we send labels with a lot of information, but as the value must be string, we use `_` as a separator. For example: * Action `Build` may have the following label: `Android_Debug_Incremental`. This is `<platform>_<build configuration>_<build type>`. * Action `LiveSync` may have the following label: `Android_Emulator_5.1`. This is `<platform>_<device type>_<device os version>`. Custom dimensions For each additional data may be send to GA via Custom Dimensions. More about them can be found [here](https://support.google.com/analytics/answer/2709828). We are using several custom dimensions, most of them have hit scope, i.e. their values are saved for the current hit (event or page), but may be changed for the next time. The only exclusion is `session id` - it has session scope, so whenever it is sent, the new value overwrites the values of the "session id" property in all hits of the current session. One interesting custom dimension is `Client` - it will be set to the value of `--analyticsClient` in case it is passed. In case not, we'll set it to "CLI" when terminal is interactive and "Unknown" when terminal is not interactive. Sessions - what is session for GA In Eqatec Analytics we call `stopMonitor` at the end of each command. So the session there is one command. In GA we've decided to keep the default way of measuring a session - a session is stopped when there are 30 inactive minutes, i.e. no data is sent from 30 minutes. This allows us to investigate the behavior and the flow of commands a user is executing in a single session (during the day for example). Client ID and User ID: Currently we do not send User ID to GA, we use Client ID instead as sending User IDs requires different logic and agreements. For us all users are anonymous. User-Agent When sending requests to GA, we need to set correct User-Agent, so GA will be able to understand the Operating System. For each OS the string is painfully specific... Current implementation works fine for Windows, Linux and macOS. Implementation In order to send data to GA, we are using a `universal-analytics` npm package. We cannot use Google's default libraries (`analytics.js`, `gtag.js`) as they are written for browser. So the only way to track data is to use the Measurement Protocol (directly send http requests). However, `universal-analytics` package provides a very good abstraction over the Measurement Protocol, so we are using it instead of implementing each call. More information about Measurement protocol: https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters
1 parent 11d1ff7 commit 2f1cfdc

21 files changed

+373
-57
lines changed

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ $injector.require("userSettingsService", "./services/user-settings-service");
3232
$injector.require("analyticsSettingsService", "./services/analytics-settings-service");
3333
$injector.requirePublic("analyticsService", "./services/analytics/analytics-service");
3434
$injector.require("eqatecAnalyticsProvider", "./services/analytics/eqatec-analytics-provider");
35+
$injector.require("googleAnalyticsProvider", "./services/analytics/google-analytics-provider");
3536

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

lib/constants.ts

+18
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,21 @@ export const enum NativePlatformStatus {
102102
requiresPrepare = "2",
103103
alreadyPrepared = "3"
104104
}
105+
106+
export const enum DebugTools {
107+
Chrome = "Chrome",
108+
Inspector = "Inspector"
109+
}
110+
111+
export const enum TrackActionNames {
112+
Build = "Build",
113+
CreateProject = "Create project",
114+
Debug = "Debug",
115+
Deploy = "Deploy",
116+
LiveSync = "LiveSync"
117+
}
118+
119+
export const enum BuildStates {
120+
Clean = "Clean",
121+
Incremental = "Incremental"
122+
}

lib/nativescript-cli-lib-bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ $injector.requirePublicClass("localBuildService", "./services/local-build-servic
1313
// We need this because some services check if (!$options.justlaunch) to start the device log after some operation.
1414
// We don't want this behaviour when the CLI is required as library.
1515
$injector.resolve("options").justlaunch = true;
16+
$injector.resolve<IStaticConfig>("staticConfig").disableAnalytics = true;

lib/services/analytics-settings-service.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,12 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService {
1111
return true;
1212
}
1313

14-
public async getUserId(): Promise<string> {
15-
let currentUserId = await this.$userSettingsService.getSettingValue<string>("USER_ID");
16-
if (!currentUserId) {
17-
currentUserId = createGUID(false);
18-
19-
this.$logger.trace(`Setting new USER_ID: ${currentUserId}.`);
20-
await this.$userSettingsService.saveSetting<string>("USER_ID", currentUserId);
21-
}
14+
public getUserId(): Promise<string> {
15+
return this.getSettingValueOrDefault("USER_ID");
16+
}
2217

23-
return currentUserId;
18+
public getClientId(): Promise<string> {
19+
return this.getSettingValueOrDefault(this.$staticConfig.ANALYTICS_INSTALLATION_ID_SETTING_NAME);
2420
}
2521

2622
public getClientName(): string {
@@ -43,5 +39,17 @@ class AnalyticsSettingsService implements IAnalyticsSettingsService {
4339
private getSessionsProjectKey(projectName: string): string {
4440
return `${AnalyticsSettingsService.SESSIONS_STARTED_KEY_PREFIX}${projectName}`;
4541
}
42+
43+
private async getSettingValueOrDefault(settingName: string): Promise<string> {
44+
let guid = await this.$userSettingsService.getSettingValue<string>(settingName);
45+
if (!guid) {
46+
guid = createGUID(false);
47+
48+
this.$logger.trace(`Setting new ${settingName}: ${guid}.`);
49+
await this.$userSettingsService.saveSetting<string>(settingName, guid);
50+
}
51+
52+
return guid;
53+
}
4654
}
4755
$injector.register("analyticsSettingsService", AnalyticsSettingsService);

lib/services/analytics/analytics-broker.ts

+30-27
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,41 @@ import { cache } from "../../common/decorators";
33
export class AnalyticsBroker implements IAnalyticsBroker {
44

55
@cache()
6-
private get $eqatecAnalyticsProvider(): IAnalyticsProvider {
7-
return this.$injector.resolve("eqatecAnalyticsProvider", { pathToBootstrap: this.pathToBootstrap });
6+
private async getEqatecAnalyticsProvider(): Promise<IAnalyticsProvider> {
7+
return this.$injector.resolve("eqatecAnalyticsProvider");
88
}
99

10-
constructor(private pathToBootstrap: string,
11-
private $injector: IInjector) { }
12-
13-
private get analyticsProviders(): IAnalyticsProvider[] {
14-
return [
15-
this.$eqatecAnalyticsProvider
16-
];
10+
@cache()
11+
private async getGoogleAnalyticsProvider(): Promise<IGoogleAnalyticsProvider> {
12+
const clientId = await this.$analyticsSettingsService.getClientId();
13+
return this.$injector.resolve("googleAnalyticsProvider", { clientId });
1714
}
1815

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.trackError(<IExceptionsTrackingInformation>trackInfo);
24-
break;
25-
case TrackingTypes.Feature:
26-
await provider.trackInformation(<IFeatureTrackingInformation>trackInfo);
27-
break;
28-
case TrackingTypes.AcceptTrackFeatureUsage:
29-
await provider.acceptFeatureUsageTracking(<IAcceptUsageReportingInformation>trackInfo);
30-
break;
31-
case TrackingTypes.Finish:
32-
await provider.finishTracking();
33-
break;
34-
default:
35-
throw new Error(`Invalid tracking type: ${trackInfo.type}`);
36-
}
16+
constructor(private $analyticsSettingsService: IAnalyticsSettingsService,
17+
private $injector: IInjector) { }
3718

19+
public async sendDataForTracking(trackInfo: ITrackingInformation): Promise<void> {
20+
const eqatecProvider = await this.getEqatecAnalyticsProvider();
21+
const googleProvider = await this.getGoogleAnalyticsProvider();
22+
23+
switch (trackInfo.type) {
24+
case TrackingTypes.Exception:
25+
await eqatecProvider.trackError(<IExceptionsTrackingInformation>trackInfo);
26+
break;
27+
case TrackingTypes.Feature:
28+
await eqatecProvider.trackInformation(<IFeatureTrackingInformation>trackInfo);
29+
break;
30+
case TrackingTypes.AcceptTrackFeatureUsage:
31+
await eqatecProvider.acceptFeatureUsageTracking(<IAcceptUsageReportingInformation>trackInfo);
32+
break;
33+
case TrackingTypes.GoogleAnalyticsData:
34+
await googleProvider.trackHit(<IGoogleAnalyticsTrackingInformation>trackInfo);
35+
break;
36+
case TrackingTypes.Finish:
37+
await eqatecProvider.finishTracking();
38+
break;
39+
default:
40+
throw new Error(`Invalid tracking type: ${trackInfo.type}`);
3841
}
3942

4043
}

lib/services/analytics/analytics-service.ts

+71-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { AnalyticsServiceBase } from "../../common/services/analytics-service-ba
22
import { ChildProcess } from "child_process";
33
import * as path from "path";
44
import { cache } from "../../common/decorators";
5+
import { isInteractive } from '../../common/helpers';
6+
import { DeviceTypes, AnalyticsClients } from "../../common/constants";
57

68
export class AnalyticsService extends AnalyticsServiceBase implements IAnalyticsService {
79
private static ANALYTICS_BROKER_START_TIMEOUT = 30 * 1000;
@@ -14,7 +16,9 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics
1416
$analyticsSettingsService: IAnalyticsSettingsService,
1517
$osInfo: IOsInfo,
1618
private $childProcess: IChildProcess,
17-
private $processService: IProcessService) {
19+
private $processService: IProcessService,
20+
private $projectDataService: IProjectDataService,
21+
private $mobileHelper: Mobile.IMobileHelper) {
1822
super($logger, $options, $staticConfig, $prompter, $userSettingsService, $analyticsSettingsService, $osInfo);
1923
}
2024

@@ -27,13 +31,73 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics
2731
}
2832

2933
public async trackAcceptFeatureUsage(settings: { acceptTrackFeatureUsage: boolean }): Promise<void> {
30-
31-
this.sendMessageToBroker(<IAcceptUsageReportingInformation> {
34+
this.sendMessageToBroker(<IAcceptUsageReportingInformation>{
3235
type: TrackingTypes.AcceptTrackFeatureUsage,
3336
acceptTrackFeatureUsage: settings.acceptTrackFeatureUsage
3437
});
3538
}
3639

40+
public async trackInGoogleAnalytics(gaSettings: IGoogleAnalyticsData): Promise<void> {
41+
await this.initAnalyticsStatuses();
42+
43+
if (!this.$staticConfig.disableAnalytics && this.analyticsStatuses[this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME] === AnalyticsStatus.enabled) {
44+
gaSettings.customDimensions = gaSettings.customDimensions || {};
45+
gaSettings.customDimensions[GoogleAnalyticsCustomDimensions.client] = this.$options.analyticsClient || (isInteractive() ? AnalyticsClients.Cli : AnalyticsClients.Unknown);
46+
47+
const googleAnalyticsData: IGoogleAnalyticsTrackingInformation = _.merge({ type: TrackingTypes.GoogleAnalyticsData }, gaSettings, { category: AnalyticsClients.Cli });
48+
return this.sendMessageToBroker(googleAnalyticsData);
49+
}
50+
}
51+
52+
public async trackEventActionInGoogleAnalytics(data: IEventActionData): Promise<void> {
53+
const device = data.device;
54+
const platform = device ? device.deviceInfo.platform : data.platform;
55+
const isForDevice = device ? !device.isEmulator : data.isForDevice;
56+
57+
let label: string = "";
58+
label = this.addDataToLabel(label, platform);
59+
60+
if (isForDevice !== null) {
61+
// In case action is Build and platform is Android, we do not know if the deviceType is emulator or device.
62+
// Just exclude the device_type in this case.
63+
const deviceType = isForDevice ? DeviceTypes.Device : (this.$mobileHelper.isAndroidPlatform(platform) ? DeviceTypes.Emulator : DeviceTypes.Simulator);
64+
label = this.addDataToLabel(label, deviceType);
65+
}
66+
67+
if (device) {
68+
label = this.addDataToLabel(label, device.deviceInfo.version);
69+
}
70+
71+
if (data.additionalData) {
72+
label = this.addDataToLabel(label, data.additionalData);
73+
}
74+
75+
const customDimensions: IStringDictionary = {};
76+
if (data.projectDir) {
77+
const projectData = this.$projectDataService.getProjectData(data.projectDir);
78+
customDimensions[GoogleAnalyticsCustomDimensions.projectType] = projectData.projectType;
79+
}
80+
81+
const googleAnalyticsEventData: IGoogleAnalyticsEventData = {
82+
googleAnalyticsDataType: GoogleAnalyticsDataType.Event,
83+
action: data.action,
84+
label,
85+
customDimensions
86+
};
87+
88+
this.$logger.trace("Will send the following information to Google Analytics:", googleAnalyticsEventData);
89+
90+
await this.trackInGoogleAnalytics(googleAnalyticsEventData);
91+
}
92+
93+
private addDataToLabel(label: string, newData: string): string {
94+
if (newData && label) {
95+
return `${label}_${newData}`;
96+
}
97+
98+
return label || newData || "";
99+
}
100+
37101
@cache()
38102
private getAnalyticsBroker(): Promise<ChildProcess> {
39103
return new Promise<ChildProcess>((resolve, reject) => {
@@ -89,9 +153,9 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics
89153
private async sendDataForTracking(featureName: string, featureValue: string): Promise<void> {
90154
await this.initAnalyticsStatuses();
91155

92-
if (this.analyticsStatuses[this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME] === AnalyticsStatus.enabled) {
156+
if (!this.$staticConfig.disableAnalytics && this.analyticsStatuses[this.$staticConfig.TRACK_FEATURE_USAGE_SETTING_NAME] === AnalyticsStatus.enabled) {
93157
return this.sendMessageToBroker(
94-
<IFeatureTrackingInformation> {
158+
<IFeatureTrackingInformation>{
95159
type: TrackingTypes.Feature,
96160
featureName: featureName,
97161
featureValue: featureValue
@@ -103,9 +167,9 @@ export class AnalyticsService extends AnalyticsServiceBase implements IAnalytics
103167
private async sendExceptionForTracking(exception: Error, message: string): Promise<void> {
104168
await this.initAnalyticsStatuses();
105169

106-
if (this.analyticsStatuses[this.$staticConfig.ERROR_REPORT_SETTING_NAME] === AnalyticsStatus.enabled) {
170+
if (!this.$staticConfig.disableAnalytics && this.analyticsStatuses[this.$staticConfig.ERROR_REPORT_SETTING_NAME] === AnalyticsStatus.enabled) {
107171
return this.sendMessageToBroker(
108-
<IExceptionsTrackingInformation> {
172+
<IExceptionsTrackingInformation>{
109173
type: TrackingTypes.Exception,
110174
exception,
111175
message

lib/services/analytics/analytics.d.ts

+14
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,17 @@ interface IAnalyticsProvider {
8181
*/
8282
finishTracking(): Promise<void>;
8383
}
84+
85+
interface IGoogleAnalyticsTrackingInformation extends IGoogleAnalyticsData, ITrackingInformation { }
86+
87+
/**
88+
* Describes methods required to track in Google Analytics.
89+
*/
90+
interface IGoogleAnalyticsProvider {
91+
/**
92+
* Tracks hit types.
93+
* @param {IGoogleAnalyticsData} data Data that has to be tracked.
94+
* @returns {Promise<void>}
95+
*/
96+
trackHit(data: IGoogleAnalyticsData): Promise<void>;
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const enum GoogleAnalyticsCustomDimensions {
2+
cliVersion = "cd1",
3+
projectType = "cd2",
4+
clientID = "cd3",
5+
sessionID = "cd4",
6+
client = "cd5",
7+
nodeVersion = "cd6"
8+
}

0 commit comments

Comments
 (0)