sentry-instrumentation.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. /*eslint-env node*/
  2. /*eslint import/no-nodejs-modules:0 */
  3. const crypto = require('crypto');
  4. const https = require('https');
  5. const os = require('os');
  6. const {
  7. NODE_ENV,
  8. SENTRY_INSTRUMENTATION,
  9. SENTRY_WEBPACK_WEBHOOK_SECRET,
  10. TRAVIS_COMMIT,
  11. TRAVIS_BRANCH,
  12. TRAVIS_PULL_REQUEST,
  13. TRAVIS_PULL_REQUEST_BRANCH,
  14. } = process.env;
  15. const IS_CI = !!TRAVIS_COMMIT;
  16. const PLUGIN_NAME = 'SentryInstrumentation';
  17. const GB_BYTE = 1073741824;
  18. const createSignature = function(secret, payload) {
  19. const hmac = crypto.createHmac('sha1', secret);
  20. return `sha1=${hmac.update(payload).digest('hex')}`;
  21. };
  22. class SentryInstrumentation {
  23. constructor() {
  24. // Only run if SENTRY_INSTRUMENTATION` is set or when in travis, only in the javascript suite that runs webpack
  25. if (!SENTRY_INSTRUMENTATION) {
  26. return;
  27. }
  28. this.initialBuild = false;
  29. this.Sentry = require('@sentry/node');
  30. require('@sentry/apm'); // This is required to patch Sentry
  31. this.Sentry.init({
  32. dsn: 'https://3d282d186d924374800aa47006227ce9@sentry.io/2053674',
  33. environment: IS_CI ? 'ci' : 'local',
  34. tracesSampleRate: 1.0,
  35. });
  36. if (IS_CI) {
  37. this.Sentry.setTag('branch', TRAVIS_PULL_REQUEST_BRANCH || TRAVIS_BRANCH);
  38. this.Sentry.setTag('pull_request', TRAVIS_PULL_REQUEST);
  39. }
  40. const cpus = os.cpus();
  41. this.Sentry.setTag('platform', os.platform());
  42. this.Sentry.setTag('arch', os.arch());
  43. this.Sentry.setTag(
  44. 'cpu',
  45. cpus && cpus.length ? `${cpus[0].model} (cores: ${cpus.length})}` : 'N/A'
  46. );
  47. }
  48. /**
  49. * Measures the file sizes of assets emitted from the entrypoints
  50. */
  51. measureAssetSizes(compilation) {
  52. if (!SENTRY_WEBPACK_WEBHOOK_SECRET) {
  53. return;
  54. }
  55. [...compilation.entrypoints].forEach(([entrypointName, entry]) =>
  56. entry.chunks.forEach(chunk =>
  57. chunk.files
  58. .filter(assetName => !assetName.endsWith('.map'))
  59. .forEach(assetName => {
  60. const asset = compilation.assets[assetName];
  61. const size = asset.size();
  62. const file = assetName;
  63. const body = JSON.stringify({
  64. file,
  65. entrypointName,
  66. size,
  67. commit: TRAVIS_COMMIT,
  68. pull_request_number: TRAVIS_PULL_REQUEST,
  69. environment: IS_CI ? 'ci' : '',
  70. node_env: NODE_ENV,
  71. });
  72. const req = https.request({
  73. host: 'product-eng-webhooks-vmrqv3f7nq-uw.a.run.app',
  74. path: '/metrics/webpack/webhook',
  75. method: 'POST',
  76. headers: {
  77. 'Content-Type': 'application/json',
  78. 'Content-Length': Buffer.byteLength(body),
  79. 'x-webpack-signature': createSignature(
  80. SENTRY_WEBPACK_WEBHOOK_SECRET,
  81. body
  82. ),
  83. },
  84. });
  85. req.end(body);
  86. })
  87. )
  88. );
  89. }
  90. measureBuildTime(startTime, endTime) {
  91. if (!this.Sentry) {
  92. return;
  93. }
  94. const transaction = this.Sentry.startTransaction({
  95. op: 'webpack-build',
  96. name: !this.initialBuild ? 'initial-build' : 'incremental-build',
  97. description: 'webpack build times',
  98. startTimestamp: startTime,
  99. trimEnd: true,
  100. });
  101. const span = transaction.startChild({
  102. op: 'build',
  103. description: 'webpack build',
  104. data: {
  105. os: `${os.platform()} ${os.arch()} v${os.release()}`,
  106. memory: os.freemem()
  107. ? `${os.freemem() / GB_BYTE} / ${os.totalmem() / GB_BYTE} GB (${(os.freemem() /
  108. os.totalmem()) *
  109. 100}% free)`
  110. : 'N/A',
  111. loadavg: os.loadavg(),
  112. },
  113. startTimestamp: startTime,
  114. });
  115. span.finish(endTime);
  116. transaction.finish();
  117. }
  118. apply(compiler) {
  119. compiler.hooks.done.tapAsync(
  120. PLUGIN_NAME,
  121. async ({compilation, startTime, endTime}, done) => {
  122. // Only record this once and only on Travis
  123. // Don't really care about asset sizes during local dev
  124. if (IS_CI && !this.initialBuild) {
  125. this.measureAssetSizes(compilation);
  126. }
  127. if (this.Sentry) {
  128. this.measureBuildTime(startTime / 1000, endTime / 1000);
  129. await this.Sentry.flush();
  130. }
  131. this.initialBuild = true;
  132. done();
  133. }
  134. );
  135. }
  136. }
  137. module.exports = SentryInstrumentation;