"Fossies" - the Fresh Open Source Software Archive

Member "angular-cli-8.3.23/packages/angular/cli/models/analytics.ts" (15 Jan 2020, 19198 Bytes) of package /linux/www/angular-cli-8.3.23.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

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