sentry-instrumentation.ts 4.9 KB

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