analytics.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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 NormalModule = require('webpack/lib/NormalModule');
  12. const webpackAllErrorMessageRe = /^([^(]+)\(\d+,\d\): (.*)$/gm;
  13. const webpackTsErrorMessageRe = /^[^(]+\(\d+,\d\): error (TS\d+):/;
  14. /**
  15. * Faster than using a RegExp, so we use this to count occurences in source code.
  16. * @param source The source to look into.
  17. * @param match The match string to look for.
  18. * @param wordBreak Whether to check for word break before and after a match was found.
  19. * @return The number of matches found.
  20. * @private
  21. */
  22. function countOccurrences(source, match, wordBreak = false) {
  23. if (match.length == 0) {
  24. return source.length + 1;
  25. }
  26. let count = 0;
  27. // We condition here so branch prediction happens out of the loop, not in it.
  28. if (wordBreak) {
  29. const re = /\w/;
  30. for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos)) {
  31. if (!(re.test(source[pos - 1] || '') || re.test(source[pos + match.length] || ''))) {
  32. count++; // 1 match, AH! AH! AH! 2 matches, AH! AH! AH!
  33. }
  34. pos -= match.length;
  35. if (pos < 0) {
  36. break;
  37. }
  38. }
  39. }
  40. else {
  41. for (let pos = source.lastIndexOf(match); pos >= 0; pos = source.lastIndexOf(match, pos)) {
  42. count++; // 1 match, AH! AH! AH! 2 matches, AH! AH! AH!
  43. pos -= match.length;
  44. if (pos < 0) {
  45. break;
  46. }
  47. }
  48. }
  49. return count;
  50. }
  51. exports.countOccurrences = countOccurrences;
  52. /**
  53. * Holder of statistics related to the build.
  54. */
  55. class AnalyticsBuildStats {
  56. constructor() {
  57. this.isIvy = false;
  58. this.errors = [];
  59. this.numberOfNgOnInit = 0;
  60. this.numberOfComponents = 0;
  61. this.initialChunkSize = 0;
  62. this.totalChunkCount = 0;
  63. this.totalChunkSize = 0;
  64. this.lazyChunkCount = 0;
  65. this.lazyChunkSize = 0;
  66. this.assetCount = 0;
  67. this.assetSize = 0;
  68. this.polyfillSize = 0;
  69. this.cssSize = 0;
  70. }
  71. }
  72. /**
  73. * Analytics plugin that reports the analytics we want from the CLI.
  74. */
  75. class NgBuildAnalyticsPlugin {
  76. constructor(_projectRoot, _analytics, _category) {
  77. this._projectRoot = _projectRoot;
  78. this._analytics = _analytics;
  79. this._category = _category;
  80. this._built = false;
  81. this._stats = new AnalyticsBuildStats();
  82. }
  83. _reset() {
  84. this._stats = new AnalyticsBuildStats();
  85. }
  86. _getMetrics(stats) {
  87. const startTime = +(stats.startTime || 0);
  88. const endTime = +(stats.endTime || 0);
  89. const metrics = [];
  90. metrics[core_1.analytics.NgCliAnalyticsMetrics.BuildTime] = (endTime - startTime);
  91. metrics[core_1.analytics.NgCliAnalyticsMetrics.NgOnInitCount] = this._stats.numberOfNgOnInit;
  92. metrics[core_1.analytics.NgCliAnalyticsMetrics.NgComponentCount] = this._stats.numberOfComponents;
  93. metrics[core_1.analytics.NgCliAnalyticsMetrics.InitialChunkSize] = this._stats.initialChunkSize;
  94. metrics[core_1.analytics.NgCliAnalyticsMetrics.TotalChunkCount] = this._stats.totalChunkCount;
  95. metrics[core_1.analytics.NgCliAnalyticsMetrics.TotalChunkSize] = this._stats.totalChunkSize;
  96. metrics[core_1.analytics.NgCliAnalyticsMetrics.LazyChunkCount] = this._stats.lazyChunkCount;
  97. metrics[core_1.analytics.NgCliAnalyticsMetrics.LazyChunkSize] = this._stats.lazyChunkSize;
  98. metrics[core_1.analytics.NgCliAnalyticsMetrics.AssetCount] = this._stats.assetCount;
  99. metrics[core_1.analytics.NgCliAnalyticsMetrics.AssetSize] = this._stats.assetSize;
  100. metrics[core_1.analytics.NgCliAnalyticsMetrics.PolyfillSize] = this._stats.polyfillSize;
  101. metrics[core_1.analytics.NgCliAnalyticsMetrics.CssSize] = this._stats.cssSize;
  102. return metrics;
  103. }
  104. _getDimensions(stats) {
  105. const dimensions = [];
  106. if (this._stats.errors.length) {
  107. // Adding commas before and after so the regex are easier to define filters.
  108. dimensions[core_1.analytics.NgCliAnalyticsDimensions.BuildErrors] = `,${this._stats.errors.join()},`;
  109. }
  110. dimensions[core_1.analytics.NgCliAnalyticsDimensions.NgIvyEnabled] = this._stats.isIvy;
  111. return dimensions;
  112. }
  113. _reportBuildMetrics(stats) {
  114. const dimensions = this._getDimensions(stats);
  115. const metrics = this._getMetrics(stats);
  116. this._analytics.event(this._category, 'build', { dimensions, metrics });
  117. }
  118. _reportRebuildMetrics(stats) {
  119. const dimensions = this._getDimensions(stats);
  120. const metrics = this._getMetrics(stats);
  121. this._analytics.event(this._category, 'rebuild', { dimensions, metrics });
  122. }
  123. _checkTsNormalModule(module) {
  124. if (module._source) {
  125. // PLEASE REMEMBER:
  126. // We're dealing with ES5 _or_ ES2015 JavaScript at this point (we don't know for sure).
  127. // Just count the ngOnInit occurences. Comments/Strings/calls occurences should be sparse
  128. // so we just consider them within the margin of error. We do break on word break though.
  129. this._stats.numberOfNgOnInit += countOccurrences(module._source.source(), 'ngOnInit', true);
  130. // Count the number of `Component({` strings (case sensitive), which happens in __decorate().
  131. // This does not include View Engine AOT compilation, we use the ngfactory for it.
  132. this._stats.numberOfComponents += countOccurrences(module._source.source(), ' Component({');
  133. // For Ivy we just count ngComponentDef.
  134. const numIvyComponents = countOccurrences(module._source.source(), 'ngComponentDef', true);
  135. this._stats.numberOfComponents += numIvyComponents;
  136. // Check whether this is an Ivy app so that it can reported as part of analytics.
  137. if (!this._stats.isIvy) {
  138. if (numIvyComponents > 0 || module._source.source().includes('ngModuleDef')) {
  139. this._stats.isIvy = true;
  140. }
  141. }
  142. }
  143. }
  144. _checkNgFactoryNormalModule(module) {
  145. if (module._source) {
  146. // PLEASE REMEMBER:
  147. // We're dealing with ES5 _or_ ES2015 JavaScript at this point (we don't know for sure).
  148. // Count the number of `.ɵccf(` strings (case sensitive). They're calls to components
  149. // factories.
  150. this._stats.numberOfComponents += countOccurrences(module._source.source(), '.ɵccf(');
  151. }
  152. }
  153. _collectErrors(stats) {
  154. if (stats.hasErrors()) {
  155. for (const errObject of stats.compilation.errors) {
  156. if (errObject instanceof Error) {
  157. const allErrors = errObject.message.match(webpackAllErrorMessageRe);
  158. for (const err of [...allErrors || []].slice(1)) {
  159. const message = (err.match(webpackTsErrorMessageRe) || [])[1];
  160. if (message) {
  161. // At this point this should be a TS1234.
  162. this._stats.errors.push(message);
  163. }
  164. }
  165. }
  166. }
  167. }
  168. }
  169. // We can safely disable no any here since we know the format of the JSON output from webpack.
  170. // tslint:disable-next-line:no-any
  171. _collectBundleStats(json) {
  172. json.chunks
  173. .filter((chunk) => chunk.rendered)
  174. .forEach((chunk) => {
  175. const asset = json.assets.find((x) => x.name == chunk.files[0]);
  176. const size = asset ? asset.size : 0;
  177. if (chunk.entry || chunk.initial) {
  178. this._stats.initialChunkSize += size;
  179. }
  180. else {
  181. this._stats.lazyChunkCount++;
  182. this._stats.lazyChunkSize += size;
  183. }
  184. this._stats.totalChunkCount++;
  185. this._stats.totalChunkSize += size;
  186. });
  187. json.assets
  188. // Filter out chunks. We only count assets that are not JS.
  189. .filter((a) => {
  190. return json.chunks.every((chunk) => chunk.files[0] != a.name);
  191. })
  192. .forEach((a) => {
  193. this._stats.assetSize += (a.size || 0);
  194. this._stats.assetCount++;
  195. });
  196. for (const asset of json.assets) {
  197. if (asset.name == 'polyfill') {
  198. this._stats.polyfillSize += asset.size || 0;
  199. }
  200. }
  201. for (const chunk of json.chunks) {
  202. if (chunk.files[0] && chunk.files[0].endsWith('.css')) {
  203. this._stats.cssSize += chunk.size || 0;
  204. }
  205. }
  206. }
  207. /************************************************************************************************
  208. * The next section is all the different Webpack hooks for this plugin.
  209. */
  210. /**
  211. * Reports a succeed module.
  212. * @private
  213. */
  214. _succeedModule(mod) {
  215. // Only report NormalModule instances.
  216. if (mod.constructor !== NormalModule) {
  217. return;
  218. }
  219. const module = mod;
  220. // Only reports modules that are part of the user's project. We also don't do node_modules.
  221. // There is a chance that someone name a file path `hello_node_modules` or something and we
  222. // will ignore that file for the purpose of gathering, but we're willing to take the risk.
  223. if (!module.resource
  224. || !module.resource.startsWith(this._projectRoot)
  225. || module.resource.indexOf('node_modules') >= 0) {
  226. return;
  227. }
  228. // Check that it's a source file from the project.
  229. if (module.resource.endsWith('.ts')) {
  230. this._checkTsNormalModule(module);
  231. }
  232. else if (module.resource.endsWith('.ngfactory.js')) {
  233. this._checkNgFactoryNormalModule(module);
  234. }
  235. }
  236. _compilation(compiler, compilation) {
  237. this._reset();
  238. compilation.hooks.succeedModule.tap('NgBuildAnalyticsPlugin', this._succeedModule.bind(this));
  239. }
  240. _done(stats) {
  241. this._collectErrors(stats);
  242. this._collectBundleStats(stats.toJson());
  243. if (this._built) {
  244. this._reportRebuildMetrics(stats);
  245. }
  246. else {
  247. this._reportBuildMetrics(stats);
  248. this._built = true;
  249. }
  250. }
  251. apply(compiler) {
  252. compiler.hooks.compilation.tap('NgBuildAnalyticsPlugin', this._compilation.bind(this, compiler));
  253. compiler.hooks.done.tap('NgBuildAnalyticsPlugin', this._done.bind(this));
  254. }
  255. }
  256. exports.NgBuildAnalyticsPlugin = NgBuildAnalyticsPlugin;