@grafana/faro-web-sdk
Version:
Faro instrumentations, metas, transports for web.
179 lines • 7.8 kB
JavaScript
import { apiMessageBus, dateNow, genShortID, Observable, stringifyObjectValues, USER_ACTION_CANCEL, USER_ACTION_END, USER_ACTION_HALT, USER_ACTION_START, } from '@grafana/faro-core';
import { MESSAGE_TYPE_HTTP_REQUEST_END, MESSAGE_TYPE_HTTP_REQUEST_START, userActionDataAttributeParsed as userActionDataAttribute, } from './const';
import { monitorDomMutations } from './domMutationMonitor';
import { monitorHttpRequests } from './httpRequestMonitor';
import { monitorPerformanceEntries } from './performanceEntriesMonitor';
import { convertDataAttributeName } from './util';
const maxFollowUpActionTimeRange = 100;
export function getUserEventHandler(faro) {
const { api, config } = faro;
const httpMonitor = monitorHttpRequests();
const domMutationsMonitor = monitorDomMutations();
const performanceEntriesMonitor = monitorPerformanceEntries();
let timeoutId;
let actionRunning = false;
function processUserEvent(event) {
var _a;
let userActionName;
const isApiEventDetected = isApiEvent(event);
if (isApiEventDetected) {
userActionName = event.name;
}
else {
userActionName = getUserActionName(event.target, (_a = config.trackUserActionsDataAttributeName) !== null && _a !== void 0 ? _a : userActionDataAttribute);
}
if (actionRunning || userActionName == null) {
return;
}
actionRunning = true;
const startTime = dateNow();
let endTime;
const actionId = genShortID();
apiMessageBus.notify({
type: USER_ACTION_START,
name: userActionName,
startTime: startTime,
parentId: actionId,
});
// Triggers if no initial action happened within the first 100ms
timeoutId = startTimeout(timeoutId, () => {
endTime = dateNow();
// Listening for follow up activities stops once action is cancelled (set to false)
actionRunning = false;
sendUserActionCancelMessage(userActionName, actionId);
}, maxFollowUpActionTimeRange);
const runningRequests = new Map();
let isHalted = false;
let pendingActionTimeoutId;
const allMonitorsSub = new Observable()
.merge(httpMonitor, domMutationsMonitor, performanceEntriesMonitor)
.takeWhile(() => actionRunning)
.filter((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((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, () => {
endTime = dateNow();
const userActionParentEventProps = Object.assign({ api,
userActionName,
startTime, endTime: endTime, actionId,
event }, (isApiEventDetected ? { attributes: event.attributes } : {}));
const hasPendingRequests = runningRequests.size > 0;
const isAllPendingRequestsResolved = isHalted && !hasPendingRequests;
if (isAllPendingRequestsResolved) {
clearTimeout(pendingActionTimeoutId);
isHalted = false;
}
if (hasPendingRequests) {
isHalted = true;
apiMessageBus.notify({
type: USER_ACTION_HALT,
name: userActionName,
parentId: actionId,
reason: 'pending-requests',
haltTime: dateNow(),
});
pendingActionTimeoutId = startTimeout(undefined, () => {
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) {
const { api, userActionName, startTime, endTime, actionId, event, attributes } = props;
const duration = endTime - startTime;
const eventType = event.type;
// order matters, first emit the user-action-end event and afterwards push the parent event
apiMessageBus.notify({
type: USER_ACTION_END,
name: userActionName,
id: actionId,
startTime,
endTime,
duration,
eventType,
});
// Send the final action parent event
api.pushEvent(userActionName, Object.assign({ userActionStartTime: startTime.toString(), userActionEndTime: endTime.toString(), userActionDuration: duration.toString(), userActionEventType: eventType }, stringifyObjectValues(attributes)), undefined, {
timestampOverwriteMs: startTime,
customPayloadTransformer: (payload) => {
payload.action = {
id: actionId,
name: userActionName,
};
return payload;
},
});
}
function getUserActionName(element, dataAttributeName) {
const parsedDataAttributeName = convertDataAttributeName(dataAttributeName);
const dataset = element.dataset;
for (const 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(() => {
cb();
}, delay);
return timeoutId;
}
function sendUserActionCancelMessage(userActionName, actionId) {
apiMessageBus.notify({
type: 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 === MESSAGE_TYPE_HTTP_REQUEST_START;
}
function isRequestEndMessage(msg) {
return msg.type === MESSAGE_TYPE_HTTP_REQUEST_END;
}
function isApiEvent(apiEvent) {
return apiEvent.type === 'apiEvent' && typeof apiEvent.name === 'string';
}
//# sourceMappingURL=processUserActionEventHandler.js.map