UNPKG

@grafana/faro-web-sdk

Version:

Faro instrumentations, metas, transports for web.

179 lines 7.8 kB
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