UNPKG

22.6 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.getSharedAnalytics = exports.getWorkspaceAnalytics = exports.hasWorkspaceAnalyticsConfiguration = exports.getGlobalAnalytics = exports.hasGlobalAnalyticsConfiguration = exports.promptProjectAnalytics = exports.promptGlobalAnalytics = exports.setAnalyticsConfig = exports.UniversalAnalytics = exports.isPackageNameSafeForAnalytics = exports.analyticsPackageSafelist = exports.AnalyticsProperties = void 0;
4/**
5 * @license
6 * Copyright Google Inc. All Rights Reserved.
7 *
8 * Use of this source code is governed by an MIT-style license that can be
9 * found in the LICENSE file at https://angular.io/license
10 */
11const core_1 = require("@angular-devkit/core");
12const child_process = require("child_process");
13const debug = require("debug");
14const fs_1 = require("fs");
15const inquirer = require("inquirer");
16const os = require("os");
17const ua = require("universal-analytics");
18const uuid_1 = require("uuid");
19const color_1 = require("../utilities/color");
20const config_1 = require("../utilities/config");
21const tty_1 = require("../utilities/tty");
22// tslint:disable: no-console
23const analyticsDebug = debug('ng:analytics'); // Generate analytics, including settings and users.
24const analyticsLogDebug = debug('ng:analytics:log'); // Actual logs of events.
25const BYTES_PER_GIGABYTES = 1024 * 1024 * 1024;
26let _defaultAngularCliPropertyCache;
27exports.AnalyticsProperties = {
28 AngularCliProd: 'UA-8594346-29',
29 AngularCliStaging: 'UA-8594346-32',
30 get AngularCliDefault() {
31 if (_defaultAngularCliPropertyCache) {
32 return _defaultAngularCliPropertyCache;
33 }
34 const v = require('../package.json').version;
35 // The logic is if it's a full version then we should use the prod GA property.
36 if (/^\d+\.\d+\.\d+$/.test(v) && v !== '0.0.0') {
37 _defaultAngularCliPropertyCache = exports.AnalyticsProperties.AngularCliProd;
38 }
39 else {
40 _defaultAngularCliPropertyCache = exports.AnalyticsProperties.AngularCliStaging;
41 }
42 return _defaultAngularCliPropertyCache;
43 },
44};
45/**
46 * This is the ultimate safelist for checking if a package name is safe to report to analytics.
47 */
48exports.analyticsPackageSafelist = [
49 /^@angular\//,
50 /^@angular-devkit\//,
51 /^@ngtools\//,
52 '@schematics/angular',
53 '@schematics/schematics',
54 '@schematics/update',
55];
56function isPackageNameSafeForAnalytics(name) {
57 return exports.analyticsPackageSafelist.some(pattern => {
58 if (typeof pattern == 'string') {
59 return pattern === name;
60 }
61 else {
62 return pattern.test(name);
63 }
64 });
65}
66exports.isPackageNameSafeForAnalytics = isPackageNameSafeForAnalytics;
67/**
68 * Attempt to get the Windows Language Code string.
69 * @private
70 */
71function _getWindowsLanguageCode() {
72 if (!os.platform().startsWith('win')) {
73 return undefined;
74 }
75 try {
76 // This is true on Windows XP, 7, 8 and 10 AFAIK. Would return empty string or fail if it
77 // doesn't work.
78 return child_process
79 .execSync('wmic.exe os get locale')
80 .toString()
81 .trim();
82 }
83 catch (_) { }
84 return undefined;
85}
86/**
87 * Get a language code.
88 * @private
89 */
90function _getLanguage() {
91 // Note: Windows does not expose the configured language by default.
92 return (process.env.LANG || // Default Unix env variable.
93 process.env.LC_CTYPE || // For C libraries. Sometimes the above isn't set.
94 process.env.LANGSPEC || // For Windows, sometimes this will be set (not always).
95 _getWindowsLanguageCode() ||
96 '??'); // ¯\_(ツ)_/¯
97}
98/**
99 * Return the number of CPUs.
100 * @private
101 */
102function _getCpuCount() {
103 const cpus = os.cpus();
104 // Return "(count)x(average speed)".
105 return cpus.length;
106}
107/**
108 * Get the first CPU's speed. It's very rare to have multiple CPUs of different speed (in most
109 * non-ARM configurations anyway), so that's all we care about.
110 * @private
111 */
112function _getCpuSpeed() {
113 const cpus = os.cpus();
114 return Math.floor(cpus[0].speed);
115}
116/**
117 * Get the amount of memory, in megabytes.
118 * @private
119 */
120function _getRamSize() {
121 // Report in gigabytes (or closest). Otherwise it's too much noise.
122 return Math.round(os.totalmem() / BYTES_PER_GIGABYTES);
123}
124/**
125 * Get the Node name and version. This returns a string like "Node 10.11", or "io.js 3.5".
126 * @private
127 */
128function _getNodeVersion() {
129 const name = process.release.name || process.argv0;
130 return name + ' ' + process.version;
131}
132/**
133 * Get a numerical MAJOR.MINOR version of node. We report this as a metric.
134 * @private
135 */
136function _getNumericNodeVersion() {
137 const p = process.version;
138 const m = p.match(/\d+\.\d+/);
139 return (m && m[0] && parseFloat(m[0])) || 0;
140}
141// These are just approximations of UA strings. We just try to fool Google Analytics to give us the
142// data we want.
143// See https://developers.whatismybrowser.com/useragents/
144const osVersionMap = {
145 darwin: {
146 '1.3.1': '10_0_4',
147 '1.4.1': '10_1_0',
148 '5.1': '10_1_1',
149 '5.2': '10_1_5',
150 '6.0.1': '10_2',
151 '6.8': '10_2_8',
152 '7.0': '10_3_0',
153 '7.9': '10_3_9',
154 '8.0': '10_4_0',
155 '8.11': '10_4_11',
156 '9.0': '10_5_0',
157 '9.8': '10_5_8',
158 '10.0': '10_6_0',
159 '10.8': '10_6_8',
160 },
161 win32: {
162 '6.3.9600': 'Windows 8.1',
163 '6.2.9200': 'Windows 8',
164 '6.1.7601': 'Windows 7 SP1',
165 '6.1.7600': 'Windows 7',
166 '6.0.6002': 'Windows Vista SP2',
167 '6.0.6000': 'Windows Vista',
168 '5.1.2600': 'Windows XP',
169 },
170};
171/**
172 * Build a fake User Agent string for OSX. This gets sent to Analytics so it shows the proper OS,
173 * versions and others.
174 * @private
175 */
176function _buildUserAgentStringForOsx() {
177 let v = osVersionMap.darwin[os.release()];
178 if (!v) {
179 // Remove 4 to tie Darwin version to OSX version, add other info.
180 const x = parseFloat(os.release());
181 if (x > 10) {
182 v = `10_` + (x - 4).toString().replace('.', '_');
183 }
184 }
185 const cpuModel = os.cpus()[0].model.match(/^[a-z]+/i);
186 const cpu = cpuModel ? cpuModel[0] : os.cpus()[0].model;
187 return `(Macintosh; ${cpu} Mac OS X ${v || os.release()})`;
188}
189/**
190 * Build a fake User Agent string for Windows. This gets sent to Analytics so it shows the proper
191 * OS, versions and others.
192 * @private
193 */
194function _buildUserAgentStringForWindows() {
195 return `(Windows NT ${os.release()})`;
196}
197/**
198 * Build a fake User Agent string for Linux. This gets sent to Analytics so it shows the proper OS,
199 * versions and others.
200 * @private
201 */
202function _buildUserAgentStringForLinux() {
203 return `(X11; Linux i686; ${os.release()}; ${os.cpus()[0].model})`;
204}
205/**
206 * Build a fake User Agent string. This gets sent to Analytics so it shows the proper OS version.
207 * @private
208 */
209function _buildUserAgentString() {
210 switch (os.platform()) {
211 case 'darwin':
212 return _buildUserAgentStringForOsx();
213 case 'win32':
214 return _buildUserAgentStringForWindows();
215 case 'linux':
216 return _buildUserAgentStringForLinux();
217 default:
218 return os.platform() + ' ' + os.release();
219 }
220}
221/**
222 * Implementation of the Analytics interface for using `universal-analytics` package.
223 */
224class UniversalAnalytics {
225 /**
226 * @param trackingId The Google Analytics ID.
227 * @param uid A User ID.
228 */
229 constructor(trackingId, uid) {
230 this._dirty = false;
231 this._metrics = [];
232 this._dimensions = [];
233 this._ua = ua(trackingId, uid, {
234 enableBatching: true,
235 batchSize: 5,
236 });
237 // Add persistent params for appVersion.
238 this._ua.set('ds', 'cli');
239 this._ua.set('ua', _buildUserAgentString());
240 this._ua.set('ul', _getLanguage());
241 // @angular/cli with version.
242 this._ua.set('an', require('../package.json').name);
243 this._ua.set('av', require('../package.json').version);
244 // We use the application ID for the Node version. This should be "node 10.10.0".
245 // We also use a custom metrics, but
246 this._ua.set('aid', _getNodeVersion());
247 // We set custom metrics for values we care about.
248 this._dimensions[core_1.analytics.NgCliAnalyticsDimensions.CpuCount] = _getCpuCount();
249 this._dimensions[core_1.analytics.NgCliAnalyticsDimensions.CpuSpeed] = _getCpuSpeed();
250 this._dimensions[core_1.analytics.NgCliAnalyticsDimensions.RamInGigabytes] = _getRamSize();
251 this._dimensions[core_1.analytics.NgCliAnalyticsDimensions.NodeVersion] = _getNumericNodeVersion();
252 }
253 /**
254 * Creates the dimension and metrics variables to pass to universal-analytics.
255 * @private
256 */
257 _customVariables(options) {
258 const additionals = {};
259 this._dimensions.forEach((v, i) => (additionals['cd' + i] = v));
260 (options.dimensions || []).forEach((v, i) => (additionals['cd' + i] = v));
261 this._metrics.forEach((v, i) => (additionals['cm' + i] = v));
262 (options.metrics || []).forEach((v, i) => (additionals['cm' + i] = v));
263 return additionals;
264 }
265 event(ec, ea, options = {}) {
266 const vars = this._customVariables(options);
267 analyticsLogDebug('event ec=%j, ea=%j, %j', ec, ea, vars);
268 const { label: el, value: ev } = options;
269 this._dirty = true;
270 this._ua.event({ ec, ea, el, ev, ...vars });
271 }
272 screenview(cd, an, options = {}) {
273 const vars = this._customVariables(options);
274 analyticsLogDebug('screenview cd=%j, an=%j, %j', cd, an, vars);
275 const { appVersion: av, appId: aid, appInstallerId: aiid } = options;
276 this._dirty = true;
277 this._ua.screenview({ cd, an, av, aid, aiid, ...vars });
278 }
279 pageview(dp, options = {}) {
280 const vars = this._customVariables(options);
281 analyticsLogDebug('pageview dp=%j, %j', dp, vars);
282 const { hostname: dh, title: dt } = options;
283 this._dirty = true;
284 this._ua.pageview({ dp, dh, dt, ...vars });
285 }
286 timing(utc, utv, utt, options = {}) {
287 const vars = this._customVariables(options);
288 analyticsLogDebug('timing utc=%j, utv=%j, utl=%j, %j', utc, utv, utt, vars);
289 const { label: utl } = options;
290 this._dirty = true;
291 this._ua.timing({ utc, utv, utt, utl, ...vars });
292 }
293 flush() {
294 if (!this._dirty) {
295 return Promise.resolve();
296 }
297 this._dirty = false;
298 return new Promise(resolve => this._ua.send(resolve));
299 }
300}
301exports.UniversalAnalytics = UniversalAnalytics;
302/**
303 * Set analytics settings. This does not work if the user is not inside a project.
304 * @param level Which config to use. "global" for user-level, and "local" for project-level.
305 * @param value Either a user ID, true to generate a new User ID, or false to disable analytics.
306 */
307function setAnalyticsConfig(level, value) {
308 analyticsDebug('setting %s level analytics to: %s', level, value);
309 const [config, configPath] = config_1.getWorkspaceRaw(level);
310 if (!config || !configPath) {
311 throw new Error(`Could not find ${level} workspace.`);
312 }
313 const configValue = config.value;
314 const cli = configValue['cli'] || (configValue['cli'] = {});
315 if (!core_1.json.isJsonObject(cli)) {
316 throw new Error(`Invalid config found at ${configPath}. CLI should be an object.`);
317 }
318 if (value === true) {
319 value = uuid_1.v4();
320 }
321 cli['analytics'] = value;
322 const output = JSON.stringify(configValue, null, 2);
323 fs_1.writeFileSync(configPath, output);
324 analyticsDebug('done');
325}
326exports.setAnalyticsConfig = setAnalyticsConfig;
327/**
328 * Prompt the user for usage gathering permission.
329 * @param force Whether to ask regardless of whether or not the user is using an interactive shell.
330 * @return Whether or not the user was shown a prompt.
331 */
332async function promptGlobalAnalytics(force = false) {
333 analyticsDebug('prompting global analytics.');
334 if (force || tty_1.isTTY()) {
335 const answers = await inquirer.prompt([
336 {
337 type: 'confirm',
338 name: 'analytics',
339 message: core_1.tags.stripIndents `
340 Would you like to share anonymous usage data with the Angular Team at Google under
341 Google’s Privacy Policy at https://policies.google.com/privacy? For more details and
342 how to change this setting, see http://angular.io/analytics.
343 `,
344 default: false,
345 },
346 ]);
347 setAnalyticsConfig('global', answers.analytics);
348 if (answers.analytics) {
349 console.log('');
350 console.log(core_1.tags.stripIndent `
351 Thank you for sharing anonymous usage data. If you change your mind, the following
352 command will disable this feature entirely:
353
354 ${color_1.colors.yellow('ng analytics off')}
355 `);
356 console.log('');
357 // Send back a ping with the user `optin`.
358 const ua = new UniversalAnalytics(exports.AnalyticsProperties.AngularCliDefault, 'optin');
359 ua.pageview('/telemetry/optin');
360 await ua.flush();
361 }
362 else {
363 // Send back a ping with the user `optout`. This is the only thing we send.
364 const ua = new UniversalAnalytics(exports.AnalyticsProperties.AngularCliDefault, 'optout');
365 ua.pageview('/telemetry/optout');
366 await ua.flush();
367 }
368 return true;
369 }
370 else {
371 analyticsDebug('Either STDOUT or STDIN are not TTY and we skipped the prompt.');
372 }
373 return false;
374}
375exports.promptGlobalAnalytics = promptGlobalAnalytics;
376/**
377 * Prompt the user for usage gathering permission for the local project. Fails if there is no
378 * local workspace.
379 * @param force Whether to ask regardless of whether or not the user is using an interactive shell.
380 * @return Whether or not the user was shown a prompt.
381 */
382async function promptProjectAnalytics(force = false) {
383 analyticsDebug('prompting user');
384 const [config, configPath] = config_1.getWorkspaceRaw('local');
385 if (!config || !configPath) {
386 throw new Error(`Could not find a local workspace. Are you in a project?`);
387 }
388 if (force || tty_1.isTTY()) {
389 const answers = await inquirer.prompt([
390 {
391 type: 'confirm',
392 name: 'analytics',
393 message: core_1.tags.stripIndents `
394 Would you like to share anonymous usage data about this project with the Angular Team at
395 Google under Google’s Privacy Policy at https://policies.google.com/privacy? For more
396 details and how to change this setting, see http://angular.io/analytics.
397
398 `,
399 default: false,
400 },
401 ]);
402 setAnalyticsConfig('local', answers.analytics);
403 if (answers.analytics) {
404 console.log('');
405 console.log(core_1.tags.stripIndent `
406 Thank you for sharing anonymous usage data. Would you change your mind, the following
407 command will disable this feature entirely:
408
409 ${color_1.colors.yellow('ng analytics project off')}
410 `);
411 console.log('');
412 // Send back a ping with the user `optin`.
413 const ua = new UniversalAnalytics(exports.AnalyticsProperties.AngularCliDefault, 'optin');
414 ua.pageview('/telemetry/project/optin');
415 await ua.flush();
416 }
417 else {
418 // Send back a ping with the user `optout`. This is the only thing we send.
419 const ua = new UniversalAnalytics(exports.AnalyticsProperties.AngularCliDefault, 'optout');
420 ua.pageview('/telemetry/project/optout');
421 await ua.flush();
422 }
423 return true;
424 }
425 return false;
426}
427exports.promptProjectAnalytics = promptProjectAnalytics;
428async function hasGlobalAnalyticsConfiguration() {
429 try {
430 const globalWorkspace = await config_1.getWorkspace('global');
431 const analyticsConfig = globalWorkspace && globalWorkspace.getCli() && globalWorkspace.getCli()['analytics'];
432 if (analyticsConfig !== null && analyticsConfig !== undefined) {
433 return true;
434 }
435 }
436 catch (_a) { }
437 return false;
438}
439exports.hasGlobalAnalyticsConfiguration = hasGlobalAnalyticsConfiguration;
440/**
441 * Get the global analytics object for the user. This returns an instance of UniversalAnalytics,
442 * or undefined if analytics are disabled.
443 *
444 * If any problem happens, it is considered the user has been opting out of analytics.
445 */
446async function getGlobalAnalytics() {
447 analyticsDebug('getGlobalAnalytics');
448 const propertyId = exports.AnalyticsProperties.AngularCliDefault;
449 if ('NG_CLI_ANALYTICS' in process.env) {
450 if (process.env['NG_CLI_ANALYTICS'] == 'false' || process.env['NG_CLI_ANALYTICS'] == '') {
451 analyticsDebug('NG_CLI_ANALYTICS is false');
452 return undefined;
453 }
454 if (process.env['NG_CLI_ANALYTICS'] === 'ci') {
455 analyticsDebug('Running in CI mode');
456 return new UniversalAnalytics(propertyId, 'ci');
457 }
458 }
459 // If anything happens we just keep the NOOP analytics.
460 try {
461 const globalWorkspace = await config_1.getWorkspace('global');
462 const analyticsConfig = globalWorkspace && globalWorkspace.getCli() && globalWorkspace.getCli()['analytics'];
463 analyticsDebug('Client Analytics config found: %j', analyticsConfig);
464 if (analyticsConfig === false) {
465 analyticsDebug('Analytics disabled. Ignoring all analytics.');
466 return undefined;
467 }
468 else if (analyticsConfig === undefined || analyticsConfig === null) {
469 analyticsDebug('Analytics settings not found. Ignoring all analytics.');
470 // globalWorkspace can be null if there is no file. analyticsConfig would be null in this
471 // case. Since there is no file, the user hasn't answered and the expected return value is
472 // undefined.
473 return undefined;
474 }
475 else {
476 let uid = undefined;
477 if (typeof analyticsConfig == 'string') {
478 uid = analyticsConfig;
479 }
480 else if (typeof analyticsConfig == 'object' && typeof analyticsConfig['uid'] == 'string') {
481 uid = analyticsConfig['uid'];
482 }
483 analyticsDebug('client id: %j', uid);
484 if (uid == undefined) {
485 return undefined;
486 }
487 return new UniversalAnalytics(propertyId, uid);
488 }
489 }
490 catch (err) {
491 analyticsDebug('Error happened during reading of analytics config: %s', err.message);
492 return undefined;
493 }
494}
495exports.getGlobalAnalytics = getGlobalAnalytics;
496async function hasWorkspaceAnalyticsConfiguration() {
497 try {
498 const globalWorkspace = await config_1.getWorkspace('local');
499 const analyticsConfig = globalWorkspace
500 && globalWorkspace.getCli()
501 && globalWorkspace.getCli()['analytics'];
502 if (analyticsConfig !== undefined) {
503 return true;
504 }
505 }
506 catch (_a) { }
507 return false;
508}
509exports.hasWorkspaceAnalyticsConfiguration = hasWorkspaceAnalyticsConfiguration;
510/**
511 * Get the workspace analytics object for the user. This returns an instance of UniversalAnalytics,
512 * or undefined if analytics are disabled.
513 *
514 * If any problem happens, it is considered the user has been opting out of analytics.
515 */
516async function getWorkspaceAnalytics() {
517 analyticsDebug('getWorkspaceAnalytics');
518 try {
519 const globalWorkspace = await config_1.getWorkspace('local');
520 const analyticsConfig = globalWorkspace === null || globalWorkspace === void 0 ? void 0 : globalWorkspace.getCli()['analytics'];
521 analyticsDebug('Workspace Analytics config found: %j', analyticsConfig);
522 if (analyticsConfig === false) {
523 analyticsDebug('Analytics disabled. Ignoring all analytics.');
524 return undefined;
525 }
526 else if (analyticsConfig === undefined || analyticsConfig === null) {
527 analyticsDebug('Analytics settings not found. Ignoring all analytics.');
528 return undefined;
529 }
530 else {
531 let uid = undefined;
532 if (typeof analyticsConfig == 'string') {
533 uid = analyticsConfig;
534 }
535 else if (typeof analyticsConfig == 'object' && typeof analyticsConfig['uid'] == 'string') {
536 uid = analyticsConfig['uid'];
537 }
538 analyticsDebug('client id: %j', uid);
539 if (uid == undefined) {
540 return undefined;
541 }
542 return new UniversalAnalytics(exports.AnalyticsProperties.AngularCliDefault, uid);
543 }
544 }
545 catch (err) {
546 analyticsDebug('Error happened during reading of analytics config: %s', err.message);
547 return undefined;
548 }
549}
550exports.getWorkspaceAnalytics = getWorkspaceAnalytics;
551/**
552 * Return the usage analytics sharing setting, which is either a property string (GA-XXXXXXX-XX),
553 * or undefined if no sharing.
554 */
555async function getSharedAnalytics() {
556 analyticsDebug('getSharedAnalytics');
557 const envVarName = 'NG_CLI_ANALYTICS_SHARE';
558 if (envVarName in process.env) {
559 if (process.env[envVarName] == 'false' || process.env[envVarName] == '') {
560 analyticsDebug('NG_CLI_ANALYTICS is false');
561 return undefined;
562 }
563 }
564 // If anything happens we just keep the NOOP analytics.
565 try {
566 const globalWorkspace = await config_1.getWorkspace('global');
567 const analyticsConfig = globalWorkspace === null || globalWorkspace === void 0 ? void 0 : globalWorkspace.getCli()['analyticsSharing'];
568 if (!analyticsConfig || !analyticsConfig.tracking || !analyticsConfig.uuid) {
569 return undefined;
570 }
571 else {
572 analyticsDebug('Analytics sharing info: %j', analyticsConfig);
573 return new UniversalAnalytics(analyticsConfig.tracking, analyticsConfig.uuid);
574 }
575 }
576 catch (err) {
577 analyticsDebug('Error happened during reading of analytics sharing config: %s', err.message);
578 return undefined;
579 }
580}
581exports.getSharedAnalytics = getSharedAnalytics;