UNPKG

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