hubextensions.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { spanToJSON } from '@sentry/core';
  2. import { logger, timestampInSeconds, uuid4 } from '@sentry/utils';
  3. import { DEBUG_BUILD } from '../debug-build.js';
  4. import { WINDOW } from '../helpers.js';
  5. import { shouldProfileTransaction, isAutomatedPageLoadTransaction, startJSSelfProfile, MAX_PROFILE_DURATION_MS, addProfileToGlobalCache } from './utils.js';
  6. /* eslint-disable complexity */
  7. /**
  8. * Safety wrapper for startTransaction for the unlikely case that transaction starts before tracing is imported -
  9. * if that happens we want to avoid throwing an error from profiling code.
  10. * see https://github.com/getsentry/sentry-javascript/issues/4731.
  11. *
  12. * @experimental
  13. */
  14. function onProfilingStartRouteTransaction(transaction) {
  15. if (!transaction) {
  16. if (DEBUG_BUILD) {
  17. logger.log('[Profiling] Transaction is undefined, skipping profiling');
  18. }
  19. return transaction;
  20. }
  21. if (shouldProfileTransaction(transaction)) {
  22. return startProfileForTransaction(transaction);
  23. }
  24. return transaction;
  25. }
  26. /**
  27. * Wraps startTransaction and stopTransaction with profiling related logic.
  28. * startProfileForTransaction is called after the call to startTransaction in order to avoid our own code from
  29. * being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
  30. */
  31. function startProfileForTransaction(transaction) {
  32. // Start the profiler and get the profiler instance.
  33. let startTimestamp;
  34. if (isAutomatedPageLoadTransaction(transaction)) {
  35. startTimestamp = timestampInSeconds() * 1000;
  36. }
  37. const profiler = startJSSelfProfile();
  38. // We failed to construct the profiler, fallback to original transaction.
  39. // No need to log anything as this has already been logged in startProfile.
  40. if (!profiler) {
  41. return transaction;
  42. }
  43. if (DEBUG_BUILD) {
  44. logger.log(`[Profiling] started profiling transaction: ${spanToJSON(transaction).description}`);
  45. }
  46. // We create "unique" transaction names to avoid concurrent transactions with same names
  47. // from being ignored by the profiler. From here on, only this transaction name should be used when
  48. // calling the profiler methods. Note: we log the original name to the user to avoid confusion.
  49. const profileId = uuid4();
  50. /**
  51. * Idempotent handler for profile stop
  52. */
  53. async function onProfileHandler() {
  54. // Check if the profile exists and return it the behavior has to be idempotent as users may call transaction.finish multiple times.
  55. if (!transaction) {
  56. return null;
  57. }
  58. // Satisfy the type checker, but profiler will always be defined here.
  59. if (!profiler) {
  60. return null;
  61. }
  62. return profiler
  63. .stop()
  64. .then((profile) => {
  65. if (maxDurationTimeoutID) {
  66. WINDOW.clearTimeout(maxDurationTimeoutID);
  67. maxDurationTimeoutID = undefined;
  68. }
  69. if (DEBUG_BUILD) {
  70. logger.log(`[Profiling] stopped profiling of transaction: ${spanToJSON(transaction).description}`);
  71. }
  72. // In case of an overlapping transaction, stopProfiling may return null and silently ignore the overlapping profile.
  73. if (!profile) {
  74. if (DEBUG_BUILD) {
  75. logger.log(
  76. `[Profiling] profiler returned null profile for: ${spanToJSON(transaction).description}`,
  77. 'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started',
  78. );
  79. }
  80. return null;
  81. }
  82. addProfileToGlobalCache(profileId, profile);
  83. return null;
  84. })
  85. .catch(error => {
  86. if (DEBUG_BUILD) {
  87. logger.log('[Profiling] error while stopping profiler:', error);
  88. }
  89. return null;
  90. });
  91. }
  92. // Enqueue a timeout to prevent profiles from running over max duration.
  93. let maxDurationTimeoutID = WINDOW.setTimeout(() => {
  94. if (DEBUG_BUILD) {
  95. logger.log(
  96. '[Profiling] max profile duration elapsed, stopping profiling for:',
  97. spanToJSON(transaction).description,
  98. );
  99. }
  100. // If the timeout exceeds, we want to stop profiling, but not finish the transaction
  101. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  102. onProfileHandler();
  103. }, MAX_PROFILE_DURATION_MS);
  104. // We need to reference the original end call to avoid creating an infinite loop
  105. const originalEnd = transaction.end.bind(transaction);
  106. /**
  107. * Wraps startTransaction and stopTransaction with profiling related logic.
  108. * startProfiling is called after the call to startTransaction in order to avoid our own code from
  109. * being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction.
  110. */
  111. function profilingWrappedTransactionEnd() {
  112. if (!transaction) {
  113. return originalEnd();
  114. }
  115. // onProfileHandler should always return the same profile even if this is called multiple times.
  116. // Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared.
  117. void onProfileHandler().then(
  118. () => {
  119. // TODO: Can we rewrite this to use attributes?
  120. // eslint-disable-next-line deprecation/deprecation
  121. transaction.setContext('profile', { profile_id: profileId, start_timestamp: startTimestamp });
  122. originalEnd();
  123. },
  124. () => {
  125. // If onProfileHandler fails, we still want to call the original finish method.
  126. originalEnd();
  127. },
  128. );
  129. return transaction;
  130. }
  131. transaction.end = profilingWrappedTransactionEnd;
  132. return transaction;
  133. }
  134. export { onProfilingStartRouteTransaction, startProfileForTransaction };
  135. //# sourceMappingURL=hubextensions.js.map