index.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import { _optionalChain, _optionalChainDelete } from '@sentry/utils';
  2. import { URL } from 'url';
  3. import { defineIntegration, convertIntegrationFnToClass, getCurrentScope } from '@sentry/core';
  4. import { logger, dynamicRequire } from '@sentry/utils';
  5. import { NODE_VERSION } from '../../nodeVersion.js';
  6. import { base64WorkerScript } from './worker-script.js';
  7. const DEFAULT_INTERVAL = 50;
  8. const DEFAULT_HANG_THRESHOLD = 5000;
  9. function log(message, ...args) {
  10. logger.log(`[ANR] ${message}`, ...args);
  11. }
  12. /**
  13. * We need to use dynamicRequire because worker_threads is not available in node < v12 and webpack error will when
  14. * targeting those versions
  15. */
  16. function getWorkerThreads() {
  17. return dynamicRequire(module, 'worker_threads');
  18. }
  19. /**
  20. * Gets contexts by calling all event processors. This relies on being called after all integrations are setup
  21. */
  22. async function getContexts(client) {
  23. let event = { message: 'ANR' };
  24. const eventHint = {};
  25. for (const processor of client.getEventProcessors()) {
  26. if (event === null) break;
  27. event = await processor(event, eventHint);
  28. }
  29. return _optionalChain([event, 'optionalAccess', _2 => _2.contexts]) || {};
  30. }
  31. const INTEGRATION_NAME = 'Anr';
  32. const _anrIntegration = ((options = {}) => {
  33. return {
  34. name: INTEGRATION_NAME,
  35. // TODO v8: Remove this
  36. setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function
  37. setup(client) {
  38. if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) {
  39. throw new Error('ANR detection requires Node 16.17.0 or later');
  40. }
  41. // setImmediate is used to ensure that all other integrations have been setup
  42. setImmediate(() => _startWorker(client, options));
  43. },
  44. };
  45. }) ;
  46. const anrIntegration = defineIntegration(_anrIntegration);
  47. /**
  48. * Starts a thread to detect App Not Responding (ANR) events
  49. *
  50. * ANR detection requires Node 16.17.0 or later
  51. *
  52. * @deprecated Use `anrIntegration()` instead.
  53. */
  54. // eslint-disable-next-line deprecation/deprecation
  55. const Anr = convertIntegrationFnToClass(INTEGRATION_NAME, anrIntegration)
  56. ;
  57. // eslint-disable-next-line deprecation/deprecation
  58. /**
  59. * Starts the ANR worker thread
  60. */
  61. async function _startWorker(client, _options) {
  62. const contexts = await getContexts(client);
  63. const dsn = client.getDsn();
  64. if (!dsn) {
  65. return;
  66. }
  67. // These will not be accurate if sent later from the worker thread
  68. _optionalChainDelete([contexts, 'access', _3 => _3.app, 'optionalAccess', _4 => delete _4.app_memory]);
  69. _optionalChainDelete([contexts, 'access', _5 => _5.device, 'optionalAccess', _6 => delete _6.free_memory]);
  70. const initOptions = client.getOptions();
  71. const sdkMetadata = client.getSdkMetadata() || {};
  72. if (sdkMetadata.sdk) {
  73. sdkMetadata.sdk.integrations = initOptions.integrations.map(i => i.name);
  74. }
  75. const options = {
  76. debug: logger.isEnabled(),
  77. dsn,
  78. environment: initOptions.environment || 'production',
  79. release: initOptions.release,
  80. dist: initOptions.dist,
  81. sdkMetadata,
  82. appRootPath: _options.appRootPath,
  83. pollInterval: _options.pollInterval || DEFAULT_INTERVAL,
  84. anrThreshold: _options.anrThreshold || DEFAULT_HANG_THRESHOLD,
  85. captureStackTrace: !!_options.captureStackTrace,
  86. staticTags: _options.staticTags || {},
  87. contexts,
  88. };
  89. if (options.captureStackTrace) {
  90. // eslint-disable-next-line @typescript-eslint/no-var-requires
  91. const inspector = require('inspector');
  92. if (!inspector.url()) {
  93. inspector.open(0);
  94. }
  95. }
  96. const { Worker } = getWorkerThreads();
  97. const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), {
  98. workerData: options,
  99. });
  100. process.on('exit', () => {
  101. worker.terminate();
  102. });
  103. const timer = setInterval(() => {
  104. try {
  105. const currentSession = getCurrentScope().getSession();
  106. // We need to copy the session object and remove the toJSON method so it can be sent to the worker
  107. // serialized without making it a SerializedSession
  108. const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined;
  109. // message the worker to tell it the main event loop is still running
  110. worker.postMessage({ session });
  111. } catch (_) {
  112. //
  113. }
  114. }, options.pollInterval);
  115. // Timer should not block exit
  116. timer.unref();
  117. worker.on('message', (msg) => {
  118. if (msg === 'session-ended') {
  119. log('ANR event sent from ANR worker. Clearing session in this thread.');
  120. getCurrentScope().setSession(undefined);
  121. }
  122. });
  123. worker.once('error', (err) => {
  124. clearInterval(timer);
  125. log('ANR worker error', err);
  126. });
  127. worker.once('exit', (code) => {
  128. clearInterval(timer);
  129. log('ANR worker exit', code);
  130. });
  131. // Ensure this thread can't block app exit
  132. worker.unref();
  133. }
  134. export { Anr, anrIntegration };
  135. //# sourceMappingURL=index.js.map