@grafana/faro-web-sdk
Version:
Faro instrumentations, metas, transports for web.
126 lines • 5.9 kB
JavaScript
import { apiMessageBus, dateNow, genShortID, merge, USER_ACTION_CANCEL_MESSAGE_TYPE, USER_ACTION_END_MESSAGE_TYPE, USER_ACTION_START_MESSAGE_TYPE, } from '@grafana/faro-core';
import { userActionDataAttributeParsed as userActionDataAttribute } from './const';
import { monitorDomMutations } from './domMutationMonitor';
import { monitorHttpRequests } from './httpRequestMonitor';
import { monitorPerformanceEntries } from './performanceEntriesMonitor';
import { convertDataAttributeName } from './util';
export function getUserEventHandler(faro) {
const { api, config } = faro;
const httpMonitor = monitorHttpRequests();
const domMutationsMonitor = monitorDomMutations();
const performanceEntriesMonitor = monitorPerformanceEntries();
let allMonitorsSub;
let allMonitorsObserver;
let timeoutId;
let actionRunning = false;
function processUserEvent(event) {
var _a;
const 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_MESSAGE_TYPE,
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);
});
allMonitorsObserver = merge(httpMonitor, domMutationsMonitor, performanceEntriesMonitor);
allMonitorsSub = allMonitorsObserver
.takeWhile(() => actionRunning)
.subscribe(() => {
// 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 duration = endTime - startTime;
const eventType = event.type;
// order matters, first emit the user-action-end event and then push the event
apiMessageBus.notify({
type: USER_ACTION_END_MESSAGE_TYPE,
name: userActionName,
id: actionId,
startTime,
endTime,
duration,
eventType,
});
// Send the final action parent event
api.pushEvent(userActionName, {
userActionStartTime: startTime.toString(),
userActionEndTime: endTime.toString(),
userActionDuration: duration.toString(),
userActionEventType: eventType,
}, undefined, {
timestampOverwriteMs: startTime,
customPayloadTransformer: (payload) => {
payload.action = {
id: actionId,
name: userActionName,
};
return payload;
},
});
// Ensure action is blocked until it is fully processed.
actionRunning = false;
allMonitorsSub === null || allMonitorsSub === void 0 ? void 0 : allMonitorsSub.unsubscribe();
allMonitorsObserver === null || allMonitorsObserver === void 0 ? void 0 : allMonitorsObserver.unsubscribeAll();
});
});
}
registerVisibilityChangeHandler(allMonitorsSub, allMonitorsObserver);
return processUserEvent;
}
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) {
const maxTimeSpanTillUserActionEnd = 100;
if (timeoutId) {
clearTimeout(timeoutId);
}
//@ts-expect-error for some reason vscode is using the node types
timeoutId = setTimeout(() => {
cb();
}, maxTimeSpanTillUserActionEnd);
return timeoutId;
}
function sendUserActionCancelMessage(userActionName, actionId) {
apiMessageBus.notify({
type: USER_ACTION_CANCEL_MESSAGE_TYPE,
name: userActionName,
parentId: actionId,
});
}
function registerVisibilityChangeHandler(allMonitorsSub, allMonitorsObserver) {
// stop monitoring in background tabs
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// Unsubscribe from all monitors when the tab goes into the background to free up resources (merge.unsubscribe() also unsubscribes from all inner observables)
// Monitors will be re-subscribed in the processEvent function when the first user action is detected
allMonitorsSub === null || allMonitorsSub === void 0 ? void 0 : allMonitorsSub.unsubscribe();
allMonitorsSub = undefined;
allMonitorsObserver === null || allMonitorsObserver === void 0 ? void 0 : allMonitorsObserver.unsubscribeAll();
allMonitorsObserver = undefined;
}
});
}
//# sourceMappingURL=processUserActionEventHandler.js.map