sentry-instrumentation.ts 4.3 KB

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