UNPKG

@atlaskit/editor-plugin-base

Version:

Base plugin for @atlaskit/editor-core

202 lines (198 loc) 9.84 kB
import { ACTION, ACTION_SUBJECT, BROWSER_FREEZE_INTERACTION_TYPE, EVENT_TYPE } from '@atlaskit/editor-common/analytics'; import { countNodes } from '@atlaskit/editor-common/count-nodes'; import { isPerformanceAPIAvailable, isPerformanceObserverAvailable } from '@atlaskit/editor-common/is-performance-api-available'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { getAnalyticsEventSeverity } from '@atlaskit/editor-common/utils/analytics'; import { PluginKey } from '@atlaskit/editor-prosemirror/state'; import { setInteractionType } from './utils/frozen-editor'; import InputLatencyTracker from './utils/input-latency-tracking'; export const frozenEditorPluginKey = new PluginKey('frozenEditor'); const DEFAULT_KEYSTROKE_SAMPLING_LIMIT = 100; const DEFAULT_SLOW_THRESHOLD = 300; export const DEFAULT_FREEZE_THRESHOLD = 600; export const NORMAL_SEVERITY_THRESHOLD = 2000; export const DEGRADED_SEVERITY_THRESHOLD = 3000; const DEFAULT_TRACK_SEVERITY_ENABLED = false; export const DEFAULT_TRACK_SEVERITY_THRESHOLD_NORMAL = 100; export const DEFAULT_TRACK_SEVERITY_THRESHOLD_DEGRADED = 500; const dispatchLongTaskEvent = contextIdentifierPlugin => (dispatchAnalyticsEvent, view, time, getNodeCount, interactionType, severity) => { var _contextIdentifierPlu, _contextIdentifierPlu2; const { state } = view; const nodesCount = getNodeCount(state); return dispatchAnalyticsEvent({ action: ACTION.BROWSER_FREEZE, actionSubject: ACTION_SUBJECT.EDITOR, attributes: { objectId: contextIdentifierPlugin === null || contextIdentifierPlugin === void 0 ? void 0 : (_contextIdentifierPlu = contextIdentifierPlugin.sharedState.currentState()) === null || _contextIdentifierPlu === void 0 ? void 0 : (_contextIdentifierPlu2 = _contextIdentifierPlu.contextIdentifierProvider) === null || _contextIdentifierPlu2 === void 0 ? void 0 : _contextIdentifierPlu2.objectId, freezeTime: time, nodeSize: state.doc.nodeSize, ...nodesCount, interactionType, severity }, eventType: EVENT_TYPE.OPERATIONAL }); }; export default (contextIdentifierPlugin => (dispatchAnalyticsEvent, inputTracking, browserFreezeTracking) => { let interactionType; let inputLatencyTracker = null; if (browserFreezeTracking !== null && browserFreezeTracking !== void 0 && browserFreezeTracking.trackInteractionType) { interactionType = setInteractionType(BROWSER_FREEZE_INTERACTION_TYPE.LOADING); } const samplingRate = inputTracking && typeof inputTracking.samplingRate === 'number' ? inputTracking.samplingRate : DEFAULT_KEYSTROKE_SAMPLING_LIMIT; // TODO: ED-26959 - get right values here based on appearance const slowThreshold = DEFAULT_SLOW_THRESHOLD; // TODO: ED-26959 - get right values here based on appearance const freezeThreshold = DEFAULT_FREEZE_THRESHOLD; const allowCountNodes = inputTracking && inputTracking.countNodes; let prevNodeCountState = null; let prevNodeCount = {}; // Cache the result as we were calling this multiple times // and has potential to be expensive const getNodeCount = state => { if (state === prevNodeCountState) { return prevNodeCount; } prevNodeCount = allowCountNodes ? countNodes(state) : {}; prevNodeCountState = state; return prevNodeCount; }; const shouldTrackSeverity = (inputTracking === null || inputTracking === void 0 ? void 0 : inputTracking.trackSeverity) || DEFAULT_TRACK_SEVERITY_ENABLED; const severityThresholdNormal = (inputTracking === null || inputTracking === void 0 ? void 0 : inputTracking.severityNormalThreshold) || DEFAULT_TRACK_SEVERITY_THRESHOLD_NORMAL; const severityThresholdDegraded = (inputTracking === null || inputTracking === void 0 ? void 0 : inputTracking.severityDegradedThreshold) || DEFAULT_TRACK_SEVERITY_THRESHOLD_DEGRADED; const createDispatchSample = (action, view) => (time, severity) => { var _contextIdentifierPlu3, _contextIdentifierPlu4; const { state } = view; const nodesCount = getNodeCount(state); const samplePayload = { action, actionSubject: ACTION_SUBJECT.EDITOR, attributes: { time, nodeSize: state.doc.nodeSize, ...nodesCount, objectId: contextIdentifierPlugin === null || contextIdentifierPlugin === void 0 ? void 0 : (_contextIdentifierPlu3 = contextIdentifierPlugin.sharedState.currentState()) === null || _contextIdentifierPlu3 === void 0 ? void 0 : (_contextIdentifierPlu4 = _contextIdentifierPlu3.contextIdentifierProvider) === null || _contextIdentifierPlu4 === void 0 ? void 0 : _contextIdentifierPlu4.objectId, severity: shouldTrackSeverity ? severity : undefined }, eventType: EVENT_TYPE.OPERATIONAL }; dispatchAnalyticsEvent(samplePayload); }; const createDispatchAverage = (action, view) => ({ mean, median, sampleSize }, severity) => { var _contextIdentifierPlu5, _contextIdentifierPlu6; const { state } = view; const nodeCount = getNodeCount(state); const averagePayload = { action, actionSubject: ACTION_SUBJECT.EDITOR, attributes: { mean, median, sampleSize, ...nodeCount, nodeSize: state.doc.nodeSize, severity: shouldTrackSeverity ? severity : undefined, objectId: contextIdentifierPlugin === null || contextIdentifierPlugin === void 0 ? void 0 : (_contextIdentifierPlu5 = contextIdentifierPlugin.sharedState.currentState()) === null || _contextIdentifierPlu5 === void 0 ? void 0 : (_contextIdentifierPlu6 = _contextIdentifierPlu5.contextIdentifierProvider) === null || _contextIdentifierPlu6 === void 0 ? void 0 : _contextIdentifierPlu6.objectId }, eventType: EVENT_TYPE.OPERATIONAL }; dispatchAnalyticsEvent(averagePayload); }; return new SafePlugin({ key: frozenEditorPluginKey, props: isPerformanceAPIAvailable() ? { handleTextInput(view) { if (browserFreezeTracking !== null && browserFreezeTracking !== void 0 && browserFreezeTracking.trackInteractionType) { interactionType = BROWSER_FREEZE_INTERACTION_TYPE.TYPING; } if (inputLatencyTracker) { const end = inputLatencyTracker.start(); // This is called after all handleTextInput events are executed which means first handleTextInput time incorporates following handleTextInput processing time // Also this is called before browser rendering so it doesn't count it. requestAnimationFrame(end); } return false; }, handleDOMEvents: browserFreezeTracking !== null && browserFreezeTracking !== void 0 && browserFreezeTracking.trackInteractionType ? { click: () => { interactionType = setInteractionType(BROWSER_FREEZE_INTERACTION_TYPE.CLICKING); return false; }, paste: () => { interactionType = setInteractionType(BROWSER_FREEZE_INTERACTION_TYPE.PASTING); return false; } } : undefined } : undefined, view(view) { if (!isPerformanceObserverAvailable()) { return {}; } inputLatencyTracker = new InputLatencyTracker({ samplingRate, slowThreshold, normalThreshold: severityThresholdNormal, degradedThreshold: severityThresholdDegraded, dispatchSample: createDispatchSample(ACTION.INPUT_PERF_SAMPLING, view), dispatchAverage: createDispatchAverage(ACTION.INPUT_PERF_SAMPLING_AVG, view), onSlowInput: time => { var _contextIdentifierPlu7, _contextIdentifierPlu8; const { state } = view; const nodesCount = getNodeCount(state); dispatchAnalyticsEvent({ action: ACTION.SLOW_INPUT, actionSubject: ACTION_SUBJECT.EDITOR, attributes: { time, nodeSize: state.doc.nodeSize, ...nodesCount, objectId: contextIdentifierPlugin === null || contextIdentifierPlugin === void 0 ? void 0 : (_contextIdentifierPlu7 = contextIdentifierPlugin.sharedState.currentState()) === null || _contextIdentifierPlu7 === void 0 ? void 0 : (_contextIdentifierPlu8 = _contextIdentifierPlu7.contextIdentifierProvider) === null || _contextIdentifierPlu8 === void 0 ? void 0 : _contextIdentifierPlu8.objectId }, eventType: EVENT_TYPE.OPERATIONAL }); } }); let observer; try { observer = new PerformanceObserver(list => { const perfEntries = list.getEntries(); for (let i = 0; i < perfEntries.length; i++) { const { duration } = perfEntries[i]; if (duration > freezeThreshold) { dispatchLongTaskEvent(contextIdentifierPlugin)(dispatchAnalyticsEvent, view, duration, getNodeCount, browserFreezeTracking !== null && browserFreezeTracking !== void 0 && browserFreezeTracking.trackInteractionType ? interactionType : undefined, getAnalyticsEventSeverity(duration, // Ignored via go/ees007 // eslint-disable-next-line @atlaskit/editor/enforce-todo-comment-format //TODO: get right values here severityThresholdNormal, severityThresholdDegraded)); } } }); // register observer for long task notifications observer.observe({ entryTypes: ['longtask'] }); } catch (e) {} return { destroy: () => { var _inputLatencyTracker, _observer; (_inputLatencyTracker = inputLatencyTracker) === null || _inputLatencyTracker === void 0 ? void 0 : _inputLatencyTracker.flush(); (_observer = observer) === null || _observer === void 0 ? void 0 : _observer.disconnect(); } }; } }); });