@atlaskit/editor-plugin-metrics
Version:
Metrics plugin for @atlaskit/editor-core
162 lines (159 loc) • 7.01 kB
JavaScript
import { bind } from 'bind-event-listener';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
import { ActiveSessionTimer } from './utils/active-session-timer';
import { getAnalyticsPayload } from './utils/analytics';
import { getNewPluginState } from './utils/get-new-plugin-state';
import { isTrWithDocChanges } from './utils/is-tr-with-doc-changes';
import { shouldSkipTr } from './utils/should-skip-tr';
export const metricsKey = new PluginKey('metricsPlugin');
export const initialPluginState = {
intentToStartEditTime: undefined,
lastSelection: undefined,
activeSessionTime: 0,
totalActionCount: 0,
contentSizeChanged: 0,
shouldPersistActiveSession: undefined,
timeOfLastTextInput: undefined,
initialContent: undefined,
actionTypeCount: {
textInputCount: 0,
nodeInsertionCount: 0,
nodeAttributeChangeCount: 0,
contentMovedCount: 0,
nodeDeletionCount: 0,
undoCount: 0,
markChangeCount: 0,
contentDeletedCount: 0
},
repeatedActionCount: 0,
safeInsertCount: 0
};
export const createPlugin = (api, userPreferencesProvider) => {
const timer = new ActiveSessionTimer(api);
return new SafePlugin({
key: metricsKey,
state: {
init: (_, state) => {
return {
...initialPluginState,
initialContent: state.doc.content
};
},
// eslint-disable-next-line @typescript-eslint/max-params
apply(tr, pluginState, oldState, newState) {
var _meta$shouldPersistAc;
// Return if transaction is remote or replaceDocument is set
if (tr.getMeta('isRemote') || tr.getMeta('replaceDocument')) {
return pluginState;
}
const meta = tr.getMeta(metricsKey);
// If the active session is stopped, reset the plugin state, and set initialContent to new doc content
if (meta && meta.stopActiveSession) {
return {
...initialPluginState,
initialContent: newState.doc.content
};
}
const shouldPersistActiveSession = (_meta$shouldPersistAc = meta === null || meta === void 0 ? void 0 : meta.shouldPersistActiveSession) !== null && _meta$shouldPersistAc !== void 0 ? _meta$shouldPersistAc : pluginState.shouldPersistActiveSession;
const hasDocChanges = isTrWithDocChanges(tr);
let intentToStartEditTime = (meta === null || meta === void 0 ? void 0 : meta.intentToStartEditTime) || pluginState.intentToStartEditTime;
const now = performance.now();
// If there is no intentToStartEditTime and there are no doc changes, return the plugin state
if (!intentToStartEditTime && !hasDocChanges && !tr.storedMarksSet) {
return {
...pluginState,
shouldPersistActiveSession
};
}
// Set intentToStartEditTime if it is not set and there are doc changes or marks are set
if (!intentToStartEditTime && (hasDocChanges || tr.storedMarksSet)) {
intentToStartEditTime = now;
}
// Start active session timer if intentToStartEditTime is defined, shouldStartTimer is true and shouldPersistActiveSession is false
// shouldPersistActiveSession is true when dragging block controls and when insert menu is open as user is interacting with the editor without making doc changes
// Timer should start when menu closes or dragging stops
if (intentToStartEditTime && meta !== null && meta !== void 0 && meta.shouldStartTimer && !shouldPersistActiveSession) {
timer.startTimer();
}
if (hasDocChanges) {
timer.startTimer();
if (shouldSkipTr(tr)) {
return {
...pluginState,
shouldPersistActiveSession
};
}
const newPluginState = getNewPluginState({
now,
intentToStartEditTime,
shouldPersistActiveSession,
tr,
pluginState,
oldState,
newState
});
return newPluginState;
}
return {
...pluginState,
lastSelection: (meta === null || meta === void 0 ? void 0 : meta.newSelection) || pluginState.lastSelection,
intentToStartEditTime,
shouldPersistActiveSession
};
}
},
view(view) {
const fireAnalyticsEvent = () => {
const pluginState = metricsKey.getState(view.state);
if (!pluginState) {
return;
}
let toolbarDocking;
if (expValEqualsNoExposure('platform_editor_controls', 'cohort', 'variant1')) {
toolbarDocking = userPreferencesProvider === null || userPreferencesProvider === void 0 ? void 0 : userPreferencesProvider.getPreference('toolbarDockingInitialPosition');
}
const payloadToSend = getAnalyticsPayload({
currentContent: view.state.doc.content,
pluginState,
toolbarDocking: toolbarDocking || undefined
});
if (pluginState && pluginState.totalActionCount > 0 && pluginState.activeSessionTime > 0) {
var _api$analytics, _api$analytics$action;
api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$action = _api$analytics.actions) === null || _api$analytics$action === void 0 ? void 0 : _api$analytics$action.fireAnalyticsEvent(payloadToSend, undefined, {
immediate: true
});
}
};
const unbindBeforeUnload = bind(window, {
type: 'beforeunload',
listener: () => {
fireAnalyticsEvent();
}
});
return {
destroy() {
fireAnalyticsEvent();
timer.cleanupTimer();
unbindBeforeUnload();
}
};
},
props: {
handleDOMEvents: {
click: view => {
var _pluginState$lastSele, _pluginState$lastSele2;
const newSelection = view.state.tr.selection;
const pluginState = api === null || api === void 0 ? void 0 : api.metrics.sharedState.currentState();
if ((pluginState === null || pluginState === void 0 ? void 0 : (_pluginState$lastSele = pluginState.lastSelection) === null || _pluginState$lastSele === void 0 ? void 0 : _pluginState$lastSele.from) !== newSelection.from && (pluginState === null || pluginState === void 0 ? void 0 : (_pluginState$lastSele2 = pluginState.lastSelection) === null || _pluginState$lastSele2 === void 0 ? void 0 : _pluginState$lastSele2.to) !== newSelection.to) {
api === null || api === void 0 ? void 0 : api.core.actions.execute(api === null || api === void 0 ? void 0 : api.metrics.commands.handleIntentToStartEdit({
newSelection
}));
}
return false;
}
}
}
});
};