@grafana/faro-web-sdk
Version:
Faro instrumentations, metas, transports for web.
190 lines • 8.53 kB
JavaScript
;
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getUserEventHandler = getUserEventHandler;
var faro_core_1 = require("@grafana/faro-core");
var const_1 = require("./const");
var domMutationMonitor_1 = require("./domMutationMonitor");
var httpRequestMonitor_1 = require("./httpRequestMonitor");
var performanceEntriesMonitor_1 = require("./performanceEntriesMonitor");
var util_1 = require("./util");
var maxFollowUpActionTimeRange = 100;
function getUserEventHandler(faro) {
var api = faro.api, config = faro.config;
var httpMonitor = (0, httpRequestMonitor_1.monitorHttpRequests)();
var domMutationsMonitor = (0, domMutationMonitor_1.monitorDomMutations)();
var performanceEntriesMonitor = (0, performanceEntriesMonitor_1.monitorPerformanceEntries)();
var timeoutId;
var actionRunning = false;
function processUserEvent(event) {
var _a;
var userActionName;
var isApiEventDetected = isApiEvent(event);
if (isApiEventDetected) {
userActionName = event.name;
}
else {
userActionName = getUserActionName(event.target, (_a = config.trackUserActionsDataAttributeName) !== null && _a !== void 0 ? _a : const_1.userActionDataAttributeParsed);
}
if (actionRunning || userActionName == null) {
return;
}
actionRunning = true;
var startTime = (0, faro_core_1.dateNow)();
var endTime;
var actionId = (0, faro_core_1.genShortID)();
faro_core_1.apiMessageBus.notify({
type: faro_core_1.USER_ACTION_START,
name: userActionName,
startTime: startTime,
parentId: actionId,
});
// Triggers if no initial action happened within the first 100ms
timeoutId = startTimeout(timeoutId, function () {
endTime = (0, faro_core_1.dateNow)();
// Listening for follow up activities stops once action is cancelled (set to false)
actionRunning = false;
sendUserActionCancelMessage(userActionName, actionId);
}, maxFollowUpActionTimeRange);
var runningRequests = new Map();
var isHalted = false;
var pendingActionTimeoutId;
var allMonitorsSub = new faro_core_1.Observable()
.merge(httpMonitor, domMutationsMonitor, performanceEntriesMonitor)
.takeWhile(function () { return actionRunning; })
.filter(function (msg) {
// If the user action is in halt state, we only keep listening to ended http requests
if (isHalted && !(isRequestEndMessage(msg) && runningRequests.has(msg.request.requestId))) {
return false;
}
return true;
})
.subscribe(function (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
runningRequests.set(msg.request.requestId, msg.request);
}
if (isRequestEndMessage(msg)) {
runningRequests.delete(msg.request.requestId);
}
// A http request, a DOM mutation or a performance entry happened so we have a follow up activity and start the timeout again
// If timeout is triggered the user action is done and we send respective messages and events
timeoutId = startTimeout(timeoutId, function () {
endTime = (0, faro_core_1.dateNow)();
var userActionParentEventProps = __assign({ api: api, userActionName: userActionName, startTime: startTime, endTime: endTime, actionId: actionId, event: event }, (isApiEventDetected ? { attributes: event.attributes } : {}));
var hasPendingRequests = runningRequests.size > 0;
var isAllPendingRequestsResolved = isHalted && !hasPendingRequests;
if (isAllPendingRequestsResolved) {
clearTimeout(pendingActionTimeoutId);
isHalted = false;
}
if (hasPendingRequests) {
isHalted = true;
faro_core_1.apiMessageBus.notify({
type: faro_core_1.USER_ACTION_HALT,
name: userActionName,
parentId: actionId,
reason: 'pending-requests',
haltTime: (0, faro_core_1.dateNow)(),
});
pendingActionTimeoutId = startTimeout(undefined, function () {
unsubscribeAllMonitors(allMonitorsSub);
endUserAction(userActionParentEventProps);
actionRunning = false;
isHalted = false;
}, 1000 * 10);
}
else {
unsubscribeAllMonitors(allMonitorsSub);
endUserAction(userActionParentEventProps);
actionRunning = false;
isHalted = false;
}
}, maxFollowUpActionTimeRange);
});
}
return processUserEvent;
}
/**
* User action was successfully completed and we send the final event(s)
*/
function endUserAction(props) {
var api = props.api, userActionName = props.userActionName, startTime = props.startTime, endTime = props.endTime, actionId = props.actionId, event = props.event, attributes = props.attributes;
var duration = endTime - startTime;
var eventType = event.type;
// order matters, first emit the user-action-end event and afterwards push the parent event
faro_core_1.apiMessageBus.notify({
type: faro_core_1.USER_ACTION_END,
name: userActionName,
id: actionId,
startTime: startTime,
endTime: endTime,
duration: duration,
eventType: eventType,
});
// Send the final action parent event
api.pushEvent(userActionName, __assign({ userActionStartTime: startTime.toString(), userActionEndTime: endTime.toString(), userActionDuration: duration.toString(), userActionEventType: eventType }, (0, faro_core_1.stringifyObjectValues)(attributes)), undefined, {
timestampOverwriteMs: startTime,
customPayloadTransformer: function (payload) {
payload.action = {
id: actionId,
name: userActionName,
};
return payload;
},
});
}
function getUserActionName(element, dataAttributeName) {
var parsedDataAttributeName = (0, util_1.convertDataAttributeName)(dataAttributeName);
var dataset = element.dataset;
for (var key in dataset) {
if (key === parsedDataAttributeName) {
return dataset[key];
}
}
return undefined;
}
function startTimeout(timeoutId, cb, delay) {
if (timeoutId) {
clearTimeout(timeoutId);
}
//@ts-expect-error for some reason vscode is using the node types
timeoutId = setTimeout(function () {
cb();
}, delay);
return timeoutId;
}
function sendUserActionCancelMessage(userActionName, actionId) {
faro_core_1.apiMessageBus.notify({
type: faro_core_1.USER_ACTION_CANCEL,
name: userActionName,
parentId: actionId,
});
}
function unsubscribeAllMonitors(allMonitorsSub) {
allMonitorsSub === null || allMonitorsSub === void 0 ? void 0 : allMonitorsSub.unsubscribe();
allMonitorsSub = undefined;
}
function isRequestStartMessage(msg) {
return msg.type === const_1.MESSAGE_TYPE_HTTP_REQUEST_START;
}
function isRequestEndMessage(msg) {
return msg.type === const_1.MESSAGE_TYPE_HTTP_REQUEST_END;
}
function isApiEvent(apiEvent) {
return apiEvent.type === 'apiEvent' && typeof apiEvent.name === 'string';
}
//# sourceMappingURL=processUserActionEventHandler.js.map