sentry-instrumentation.ts 5.2 KB

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