idletransaction.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. Object.defineProperty(exports, '__esModule', { value: true });
  2. const utils = require('@sentry/utils');
  3. const debugBuild = require('../debug-build.js');
  4. const spanUtils = require('../utils/spanUtils.js');
  5. const span = require('./span.js');
  6. const transaction = require('./transaction.js');
  7. const TRACING_DEFAULTS = {
  8. idleTimeout: 1000,
  9. finalTimeout: 30000,
  10. heartbeatInterval: 5000,
  11. };
  12. const FINISH_REASON_TAG = 'finishReason';
  13. const IDLE_TRANSACTION_FINISH_REASONS = [
  14. 'heartbeatFailed',
  15. 'idleTimeout',
  16. 'documentHidden',
  17. 'finalTimeout',
  18. 'externalFinish',
  19. 'cancelled',
  20. ];
  21. /**
  22. * @inheritDoc
  23. */
  24. class IdleTransactionSpanRecorder extends span.SpanRecorder {
  25. constructor(
  26. _pushActivity,
  27. _popActivity,
  28. transactionSpanId,
  29. maxlen,
  30. ) {
  31. super(maxlen);this._pushActivity = _pushActivity;this._popActivity = _popActivity;this.transactionSpanId = transactionSpanId; }
  32. /**
  33. * @inheritDoc
  34. */
  35. add(span) {
  36. // We should make sure we do not push and pop activities for
  37. // the transaction that this span recorder belongs to.
  38. if (span.spanContext().spanId !== this.transactionSpanId) {
  39. // We patch span.end() to pop an activity after setting an endTimestamp.
  40. // eslint-disable-next-line @typescript-eslint/unbound-method
  41. const originalEnd = span.end;
  42. span.end = (...rest) => {
  43. this._popActivity(span.spanContext().spanId);
  44. return originalEnd.apply(span, rest);
  45. };
  46. // We should only push new activities if the span does not have an end timestamp.
  47. if (spanUtils.spanToJSON(span).timestamp === undefined) {
  48. this._pushActivity(span.spanContext().spanId);
  49. }
  50. }
  51. super.add(span);
  52. }
  53. }
  54. /**
  55. * An IdleTransaction is a transaction that automatically finishes. It does this by tracking child spans as activities.
  56. * You can have multiple IdleTransactions active, but if the `onScope` option is specified, the idle transaction will
  57. * put itself on the scope on creation.
  58. */
  59. class IdleTransaction extends transaction.Transaction {
  60. // Activities store a list of active spans
  61. // Track state of activities in previous heartbeat
  62. // Amount of times heartbeat has counted. Will cause transaction to finish after 3 beats.
  63. // We should not use heartbeat if we finished a transaction
  64. // Idle timeout was canceled and we should finish the transaction with the last span end.
  65. /**
  66. * Timer that tracks Transaction idleTimeout
  67. */
  68. /**
  69. * @deprecated Transactions will be removed in v8. Use spans instead.
  70. */
  71. constructor(
  72. transactionContext,
  73. _idleHub,
  74. /**
  75. * The time to wait in ms until the idle transaction will be finished. This timer is started each time
  76. * there are no active spans on this transaction.
  77. */
  78. _idleTimeout = TRACING_DEFAULTS.idleTimeout,
  79. /**
  80. * The final value in ms that a transaction cannot exceed
  81. */
  82. _finalTimeout = TRACING_DEFAULTS.finalTimeout,
  83. _heartbeatInterval = TRACING_DEFAULTS.heartbeatInterval,
  84. // Whether or not the transaction should put itself on the scope when it starts and pop itself off when it ends
  85. _onScope = false,
  86. /**
  87. * When set to `true`, will disable the idle timeout (`_idleTimeout` option) and heartbeat mechanisms (`_heartbeatInterval`
  88. * option) until the `sendAutoFinishSignal()` method is called. The final timeout mechanism (`_finalTimeout` option)
  89. * will not be affected by this option, meaning the transaction will definitely be finished when the final timeout is
  90. * reached, no matter what this option is configured to.
  91. *
  92. * Defaults to `false`.
  93. */
  94. delayAutoFinishUntilSignal = false,
  95. ) {
  96. super(transactionContext, _idleHub);this._idleHub = _idleHub;this._idleTimeout = _idleTimeout;this._finalTimeout = _finalTimeout;this._heartbeatInterval = _heartbeatInterval;this._onScope = _onScope;
  97. this.activities = {};
  98. this._heartbeatCounter = 0;
  99. this._finished = false;
  100. this._idleTimeoutCanceledPermanently = false;
  101. this._beforeFinishCallbacks = [];
  102. this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[4];
  103. this._autoFinishAllowed = !delayAutoFinishUntilSignal;
  104. if (_onScope) {
  105. // We set the transaction here on the scope so error events pick up the trace
  106. // context and attach it to the error.
  107. debugBuild.DEBUG_BUILD && utils.logger.log(`Setting idle transaction on scope. Span ID: ${this.spanContext().spanId}`);
  108. // eslint-disable-next-line deprecation/deprecation
  109. _idleHub.getScope().setSpan(this);
  110. }
  111. if (!delayAutoFinishUntilSignal) {
  112. this._restartIdleTimeout();
  113. }
  114. setTimeout(() => {
  115. if (!this._finished) {
  116. this.setStatus('deadline_exceeded');
  117. this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[3];
  118. this.end();
  119. }
  120. }, this._finalTimeout);
  121. }
  122. /** {@inheritDoc} */
  123. end(endTimestamp) {
  124. const endTimestampInS = spanUtils.spanTimeInputToSeconds(endTimestamp);
  125. this._finished = true;
  126. this.activities = {};
  127. // eslint-disable-next-line deprecation/deprecation
  128. if (this.op === 'ui.action.click') {
  129. this.setAttribute(FINISH_REASON_TAG, this._finishReason);
  130. }
  131. // eslint-disable-next-line deprecation/deprecation
  132. if (this.spanRecorder) {
  133. debugBuild.DEBUG_BUILD &&
  134. // eslint-disable-next-line deprecation/deprecation
  135. utils.logger.log('[Tracing] finishing IdleTransaction', new Date(endTimestampInS * 1000).toISOString(), this.op);
  136. for (const callback of this._beforeFinishCallbacks) {
  137. callback(this, endTimestampInS);
  138. }
  139. // eslint-disable-next-line deprecation/deprecation
  140. this.spanRecorder.spans = this.spanRecorder.spans.filter((span) => {
  141. // If we are dealing with the transaction itself, we just return it
  142. if (span.spanContext().spanId === this.spanContext().spanId) {
  143. return true;
  144. }
  145. // We cancel all pending spans with status "cancelled" to indicate the idle transaction was finished early
  146. if (!spanUtils.spanToJSON(span).timestamp) {
  147. span.setStatus('cancelled');
  148. span.end(endTimestampInS);
  149. debugBuild.DEBUG_BUILD &&
  150. utils.logger.log('[Tracing] cancelling span since transaction ended early', JSON.stringify(span, undefined, 2));
  151. }
  152. const { start_timestamp: startTime, timestamp: endTime } = spanUtils.spanToJSON(span);
  153. const spanStartedBeforeTransactionFinish = startTime && startTime < endTimestampInS;
  154. // Add a delta with idle timeout so that we prevent false positives
  155. const timeoutWithMarginOfError = (this._finalTimeout + this._idleTimeout) / 1000;
  156. const spanEndedBeforeFinalTimeout = endTime && startTime && endTime - startTime < timeoutWithMarginOfError;
  157. if (debugBuild.DEBUG_BUILD) {
  158. const stringifiedSpan = JSON.stringify(span, undefined, 2);
  159. if (!spanStartedBeforeTransactionFinish) {
  160. utils.logger.log('[Tracing] discarding Span since it happened after Transaction was finished', stringifiedSpan);
  161. } else if (!spanEndedBeforeFinalTimeout) {
  162. utils.logger.log('[Tracing] discarding Span since it finished after Transaction final timeout', stringifiedSpan);
  163. }
  164. }
  165. return spanStartedBeforeTransactionFinish && spanEndedBeforeFinalTimeout;
  166. });
  167. debugBuild.DEBUG_BUILD && utils.logger.log('[Tracing] flushing IdleTransaction');
  168. } else {
  169. debugBuild.DEBUG_BUILD && utils.logger.log('[Tracing] No active IdleTransaction');
  170. }
  171. // if `this._onScope` is `true`, the transaction put itself on the scope when it started
  172. if (this._onScope) {
  173. // eslint-disable-next-line deprecation/deprecation
  174. const scope = this._idleHub.getScope();
  175. // eslint-disable-next-line deprecation/deprecation
  176. if (scope.getTransaction() === this) {
  177. // eslint-disable-next-line deprecation/deprecation
  178. scope.setSpan(undefined);
  179. }
  180. }
  181. return super.end(endTimestamp);
  182. }
  183. /**
  184. * Register a callback function that gets executed before the transaction finishes.
  185. * Useful for cleanup or if you want to add any additional spans based on current context.
  186. *
  187. * This is exposed because users have no other way of running something before an idle transaction
  188. * finishes.
  189. */
  190. registerBeforeFinishCallback(callback) {
  191. this._beforeFinishCallbacks.push(callback);
  192. }
  193. /**
  194. * @inheritDoc
  195. */
  196. initSpanRecorder(maxlen) {
  197. // eslint-disable-next-line deprecation/deprecation
  198. if (!this.spanRecorder) {
  199. const pushActivity = (id) => {
  200. if (this._finished) {
  201. return;
  202. }
  203. this._pushActivity(id);
  204. };
  205. const popActivity = (id) => {
  206. if (this._finished) {
  207. return;
  208. }
  209. this._popActivity(id);
  210. };
  211. // eslint-disable-next-line deprecation/deprecation
  212. this.spanRecorder = new IdleTransactionSpanRecorder(pushActivity, popActivity, this.spanContext().spanId, maxlen);
  213. // Start heartbeat so that transactions do not run forever.
  214. debugBuild.DEBUG_BUILD && utils.logger.log('Starting heartbeat');
  215. this._pingHeartbeat();
  216. }
  217. // eslint-disable-next-line deprecation/deprecation
  218. this.spanRecorder.add(this);
  219. }
  220. /**
  221. * Cancels the existing idle timeout, if there is one.
  222. * @param restartOnChildSpanChange Default is `true`.
  223. * If set to false the transaction will end
  224. * with the last child span.
  225. */
  226. cancelIdleTimeout(
  227. endTimestamp,
  228. {
  229. restartOnChildSpanChange,
  230. }
  231. = {
  232. restartOnChildSpanChange: true,
  233. },
  234. ) {
  235. this._idleTimeoutCanceledPermanently = restartOnChildSpanChange === false;
  236. if (this._idleTimeoutID) {
  237. clearTimeout(this._idleTimeoutID);
  238. this._idleTimeoutID = undefined;
  239. if (Object.keys(this.activities).length === 0 && this._idleTimeoutCanceledPermanently) {
  240. this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[5];
  241. this.end(endTimestamp);
  242. }
  243. }
  244. }
  245. /**
  246. * Temporary method used to externally set the transaction's `finishReason`
  247. *
  248. * ** WARNING**
  249. * This is for the purpose of experimentation only and will be removed in the near future, do not use!
  250. *
  251. * @internal
  252. *
  253. */
  254. setFinishReason(reason) {
  255. this._finishReason = reason;
  256. }
  257. /**
  258. * Permits the IdleTransaction to automatically end itself via the idle timeout and heartbeat mechanisms when the `delayAutoFinishUntilSignal` option was set to `true`.
  259. */
  260. sendAutoFinishSignal() {
  261. if (!this._autoFinishAllowed) {
  262. debugBuild.DEBUG_BUILD && utils.logger.log('[Tracing] Received finish signal for idle transaction.');
  263. this._restartIdleTimeout();
  264. this._autoFinishAllowed = true;
  265. }
  266. }
  267. /**
  268. * Restarts idle timeout, if there is no running idle timeout it will start one.
  269. */
  270. _restartIdleTimeout(endTimestamp) {
  271. this.cancelIdleTimeout();
  272. this._idleTimeoutID = setTimeout(() => {
  273. if (!this._finished && Object.keys(this.activities).length === 0) {
  274. this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[1];
  275. this.end(endTimestamp);
  276. }
  277. }, this._idleTimeout);
  278. }
  279. /**
  280. * Start tracking a specific activity.
  281. * @param spanId The span id that represents the activity
  282. */
  283. _pushActivity(spanId) {
  284. this.cancelIdleTimeout(undefined, { restartOnChildSpanChange: !this._idleTimeoutCanceledPermanently });
  285. debugBuild.DEBUG_BUILD && utils.logger.log(`[Tracing] pushActivity: ${spanId}`);
  286. this.activities[spanId] = true;
  287. debugBuild.DEBUG_BUILD && utils.logger.log('[Tracing] new activities count', Object.keys(this.activities).length);
  288. }
  289. /**
  290. * Remove an activity from usage
  291. * @param spanId The span id that represents the activity
  292. */
  293. _popActivity(spanId) {
  294. if (this.activities[spanId]) {
  295. debugBuild.DEBUG_BUILD && utils.logger.log(`[Tracing] popActivity ${spanId}`);
  296. // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
  297. delete this.activities[spanId];
  298. debugBuild.DEBUG_BUILD && utils.logger.log('[Tracing] new activities count', Object.keys(this.activities).length);
  299. }
  300. if (Object.keys(this.activities).length === 0) {
  301. const endTimestamp = utils.timestampInSeconds();
  302. if (this._idleTimeoutCanceledPermanently) {
  303. if (this._autoFinishAllowed) {
  304. this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[5];
  305. this.end(endTimestamp);
  306. }
  307. } else {
  308. // We need to add the timeout here to have the real endtimestamp of the transaction
  309. // Remember timestampInSeconds is in seconds, timeout is in ms
  310. this._restartIdleTimeout(endTimestamp + this._idleTimeout / 1000);
  311. }
  312. }
  313. }
  314. /**
  315. * Checks when entries of this.activities are not changing for 3 beats.
  316. * If this occurs we finish the transaction.
  317. */
  318. _beat() {
  319. // We should not be running heartbeat if the idle transaction is finished.
  320. if (this._finished) {
  321. return;
  322. }
  323. const heartbeatString = Object.keys(this.activities).join('');
  324. if (heartbeatString === this._prevHeartbeatString) {
  325. this._heartbeatCounter++;
  326. } else {
  327. this._heartbeatCounter = 1;
  328. }
  329. this._prevHeartbeatString = heartbeatString;
  330. if (this._heartbeatCounter >= 3) {
  331. if (this._autoFinishAllowed) {
  332. debugBuild.DEBUG_BUILD && utils.logger.log('[Tracing] Transaction finished because of no change for 3 heart beats');
  333. this.setStatus('deadline_exceeded');
  334. this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[0];
  335. this.end();
  336. }
  337. } else {
  338. this._pingHeartbeat();
  339. }
  340. }
  341. /**
  342. * Pings the heartbeat
  343. */
  344. _pingHeartbeat() {
  345. debugBuild.DEBUG_BUILD && utils.logger.log(`pinging Heartbeat -> current counter: ${this._heartbeatCounter}`);
  346. setTimeout(() => {
  347. this._beat();
  348. }, this._heartbeatInterval);
  349. }
  350. }
  351. exports.IdleTransaction = IdleTransaction;
  352. exports.IdleTransactionSpanRecorder = IdleTransactionSpanRecorder;
  353. exports.TRACING_DEFAULTS = TRACING_DEFAULTS;
  354. //# sourceMappingURL=idletransaction.js.map