@grafana/faro-web-sdk
Version:
Faro instrumentations, metas, transports for web.
117 lines • 4.87 kB
JavaScript
// packages/web-sdk/src/instrumentations/userActions/userActionController.ts
import { Observable, UserActionState } from '@grafana/faro-core';
import { monitorDomMutations } from '../_internal/monitors/domMutationMonitor';
import { monitorHttpRequests } from '../_internal/monitors/httpRequestMonitor';
import { monitorPerformanceEntries } from '../_internal/monitors/performanceEntriesMonitor';
import { isRequestEndMessage, isRequestStartMessage, startTimeout } from './util';
const defaultFollowUpActionTimeRange = 100;
const defaultHaltTimeout = 10 * 1000;
export class UserActionController {
constructor(userAction) {
this.userAction = userAction;
this.http = monitorHttpRequests();
this.dom = monitorDomMutations();
this.perf = monitorPerformanceEntries();
this.isValid = false;
this.runningRequests = new Map();
}
attach() {
// Subscribe to monitors while action is active/halting
this.allMonitorsSub = new Observable()
.merge(this.http, this.dom, this.perf)
.takeWhile(() => [UserActionState.Started, UserActionState.Halted].includes(this.userAction.getState()))
.filter((msg) => {
// If the user action is in halt state, we only keep listening to ended http requests
if (this.userAction.getState() === UserActionState.Halted &&
!(isRequestEndMessage(msg) && this.runningRequests.has(msg.request.requestId))) {
return false;
}
return true;
})
.subscribe((msg) => {
if (isRequestStartMessage(msg)) {
// An action is on halt if it has pending items, like pending HTTP requests.
// In this case we start a separate timeout to wait for the requests to finish
// If in the halt state, we stop adding Faro signals to the action's buffer (see userActionLifecycleHandler.ts)
// But we are still subscribed to
this.runningRequests.set(msg.request.requestId, msg.request);
}
if (isRequestEndMessage(msg)) {
this.runningRequests.delete(msg.request.requestId);
}
if (!isRequestEndMessage(msg)) {
if (!this.isValid) {
this.isValid = true;
}
this.scheduleFollowUp();
}
else if (this.userAction.getState() === UserActionState.Halted && this.runningRequests.size === 0) {
this.endAction();
}
});
// When UA ends or cancels, cleanup timers/subscriptions
this.stateSub = this.userAction
.filter((s) => [UserActionState.Ended, UserActionState.Cancelled].includes(s))
.first()
.subscribe(() => this.cleanup());
// initial follow-up window in case nothing else happens
this.scheduleFollowUp();
}
scheduleFollowUp() {
this.clearTimer(this.followUpTid);
this.followUpTid = setTimeout(() => {
// If action just started and there's pending work, go to halted
if (this.userAction.getState() === UserActionState.Started && this.runningRequests.size > 0) {
this.haltAction();
return;
}
// If we saw any relevant activity in the window, finish as ended
if (this.isValid) {
this.endAction();
return;
}
// Otherwise, no signals => cancel
this.cancelAction();
}, defaultFollowUpActionTimeRange);
}
haltAction() {
if (this.userAction.getState() !== UserActionState.Started) {
return;
}
this.userAction.halt();
this.startHaltTimeout();
}
startHaltTimeout() {
this.clearTimer(this.haltTid);
this.haltTid = startTimeout(this.haltTid, () => {
// If still halted after timeout, end
if (this.userAction.getState() === UserActionState.Halted) {
this.endAction();
}
}, defaultHaltTimeout);
}
endAction() {
this.userAction.end();
this.cleanup();
}
cancelAction() {
this.userAction.cancel();
this.cleanup();
}
cleanup() {
var _a, _b;
this.clearTimer(this.followUpTid);
this.clearTimer(this.haltTid);
(_a = this.allMonitorsSub) === null || _a === void 0 ? void 0 : _a.unsubscribe();
(_b = this.stateSub) === null || _b === void 0 ? void 0 : _b.unsubscribe();
this.allMonitorsSub = undefined;
this.stateSub = undefined;
this.runningRequests.clear();
}
clearTimer(id) {
if (id) {
clearTimeout(id);
}
}
}
//# sourceMappingURL=userActionController.js.map