analytics.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. "use strict";
  2. Object.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. */
  10. const core_1 = require("@angular-devkit/core");
  11. const child_process = require("child_process");
  12. const debug = require("debug");
  13. const fs_1 = require("fs");
  14. const inquirer = require("inquirer");
  15. const os = require("os");
  16. const ua = require("universal-analytics");
  17. const uuid_1 = require("uuid");
  18. const color_1 = require("../utilities/color");
  19. const config_1 = require("../utilities/config");
  20. const tty_1 = require("../utilities/tty");
  21. // tslint:disable: no-console
  22. const analyticsDebug = debug('ng:analytics'); // Generate analytics, including settings and users.
  23. const analyticsLogDebug = debug('ng:analytics:log'); // Actual logs of events.
  24. const BYTES_PER_GIGABYTES = 1024 * 1024 * 1024;
  25. let _defaultAngularCliPropertyCache;
  26. exports.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. */
  47. exports.analyticsPackageSafelist = [
  48. /^@angular\//,
  49. /^@angular-devkit\//,
  50. /^@ngtools\//,
  51. '@schematics/angular',
  52. '@schematics/schematics',
  53. '@schematics/update',
  54. ];
  55. function 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. }
  65. exports.isPackageNameSafeForAnalytics = isPackageNameSafeForAnalytics;
  66. /**
  67. * Attempt to get the Windows Language Code string.
  68. * @private
  69. */
  70. function _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. */
  89. function _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. */
  101. function _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. */
  111. function _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. */
  119. function _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. */
  127. function _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. */
  139. function _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/
  147. const 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. */
  179. function _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. */
  197. function _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. */
  205. function _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. */
  212. function _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. */
  227. class 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. }
  304. exports.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. */
  310. function 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. }
  329. exports.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. */
  335. async 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. ${color_1.colors.yellow('ng analytics off')}
  357. `);
  358. console.log('');
  359. // Send back a ping with the user `optin`.
  360. const ua = new UniversalAnalytics(exports.AnalyticsProperties.AngularCliDefault, 'optin');
  361. ua.pageview('/telemetry/optin');
  362. await ua.flush();
  363. }
  364. else {
  365. // Send back a ping with the user `optout`. This is the only thing we send.
  366. const ua = new UniversalAnalytics(exports.AnalyticsProperties.AngularCliDefault, 'optout');
  367. ua.pageview('/telemetry/optout');
  368. await ua.flush();
  369. }
  370. return true;
  371. }
  372. else {
  373. analyticsDebug('Either STDOUT or STDIN are not TTY and we skipped the prompt.');
  374. }
  375. return false;
  376. }
  377. exports.promptGlobalAnalytics = promptGlobalAnalytics;
  378. /**
  379. * Prompt the user for usage gathering permission for the local project. Fails if there is no
  380. * local workspace.
  381. * @param force Whether to ask regardless of whether or not the user is using an interactive shell.
  382. * @return Whether or not the user was shown a prompt.
  383. */
  384. async function promptProjectAnalytics(force = false) {
  385. analyticsDebug('prompting user');
  386. const [config, configPath] = config_1.getWorkspaceRaw('local');
  387. if (!config || !configPath) {
  388. throw new Error(`Could not find a local workspace. Are you in a project?`);
  389. }
  390. if (force || tty_1.isTTY()) {
  391. const answers = await inquirer.prompt([
  392. {
  393. type: 'confirm',
  394. name: 'analytics',
  395. message: core_1.tags.stripIndents `
  396. Would you like to share anonymous usage data about this project with the Angular Team at
  397. Google under Google’s Privacy Policy at https://policies.google.com/privacy? For more
  398. details and how to change this setting, see http://angular.io/analytics.
  399. `,
  400. default: false,
  401. },
  402. ]);
  403. setAnalyticsConfig('local', answers.analytics);
  404. if (answers.analytics) {
  405. console.log('');
  406. console.log(core_1.tags.stripIndent `
  407. Thank you for sharing anonymous usage data. Would you change your mind, the following
  408. command will disable this feature entirely:
  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. }
  427. exports.promptProjectAnalytics = promptProjectAnalytics;
  428. function hasGlobalAnalyticsConfiguration() {
  429. try {
  430. const globalWorkspace = 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. }
  439. exports.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. */
  446. 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 = 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. }
  495. exports.getGlobalAnalytics = getGlobalAnalytics;
  496. /**
  497. * Return the usage analytics sharing setting, which is either a property string (GA-XXXXXXX-XX),
  498. * or undefined if no sharing.
  499. */
  500. function getSharedAnalytics() {
  501. analyticsDebug('getSharedAnalytics');
  502. const envVarName = 'NG_CLI_ANALYTICS_SHARE';
  503. if (envVarName in process.env) {
  504. if (process.env[envVarName] == 'false' || process.env[envVarName] == '') {
  505. analyticsDebug('NG_CLI_ANALYTICS is false');
  506. return undefined;
  507. }
  508. }
  509. // If anything happens we just keep the NOOP analytics.
  510. try {
  511. const globalWorkspace = config_1.getWorkspace('global');
  512. const analyticsConfig = globalWorkspace && globalWorkspace.getCli() && globalWorkspace.getCli()['analyticsSharing'];
  513. if (!analyticsConfig || !analyticsConfig.tracking || !analyticsConfig.uuid) {
  514. return undefined;
  515. }
  516. else {
  517. analyticsDebug('Analytics sharing info: %j', analyticsConfig);
  518. return new UniversalAnalytics(analyticsConfig.tracking, analyticsConfig.uuid);
  519. }
  520. }
  521. catch (err) {
  522. analyticsDebug('Error happened during reading of analytics sharing config: %s', err.message);
  523. return undefined;
  524. }
  525. }
  526. exports.getSharedAnalytics = getSharedAnalytics;