sentry-instrumentation.ts 5.1 KB

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