UNPKG

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