UNPKG

@atlaskit/editor-plugin-analytics

Version:

Analytics plugin for @atlaskit/editor-core

253 lines (247 loc) 9.59 kB
import { useLayoutEffect } from 'react'; import { AnalyticsStep } from '@atlaskit/adf-schema/steps'; import { useAnalyticsEvents } from '@atlaskit/analytics-next/useAnalyticsEvents'; import { ACTION, EVENT_TYPE, fireAnalyticsEvent, getAnalyticsEventsFromTransaction } from '@atlaskit/editor-common/analytics'; import { isPerformanceAPIAvailable } from '@atlaskit/editor-common/is-performance-api-available'; import { measureRender } from '@atlaskit/editor-common/performance/measure-render'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { createAttachPayloadIntoTransaction } from './pm-plugins/analytics-api/attach-payload-into-transaction'; import { getStateContext } from './pm-plugins/analytics-api/editor-state-context'; import { editorAnalyticsChannel } from './pm-plugins/consts'; import { analyticsPluginKey } from './pm-plugins/plugin-key'; import { generateUndoRedoInputSoucePayload } from './pm-plugins/undo-redo-input-source'; function createPlugin(options, featureFlags) { if (!options) { return; } const hasRequiredPerformanceAPIs = isPerformanceAPIAvailable(); return new SafePlugin({ key: analyticsPluginKey, state: { init: () => { return { ...options, fireAnalytics: fireAnalyticsEvent(options.createAnalyticsEvent) }; }, apply: (tr, pluginState, _, state) => { var _tr$getMeta; const { createAnalyticsEvent } = (_tr$getMeta = tr.getMeta(analyticsPluginKey)) !== null && _tr$getMeta !== void 0 ? _tr$getMeta : {}; // When the createAnalyticsEvent is reconfigured if (options.createAnalyticsEvent && options.createAnalyticsEvent !== pluginState.createAnalyticsEvent || pluginState.createAnalyticsEvent !== createAnalyticsEvent && createAnalyticsEvent) { var _options$createAnalyt; return { ...pluginState, createAnalyticsEvent: (_options$createAnalyt = options.createAnalyticsEvent) !== null && _options$createAnalyt !== void 0 ? _options$createAnalyt : createAnalyticsEvent }; } if (featureFlags.catchAllTracking) { const analyticsEventWithChannel = getAnalyticsEventsFromTransaction(tr); if (analyticsEventWithChannel.length > 0) { for (const { payload, channel } of analyticsEventWithChannel) { // Measures how much time it takes to update the DOM after each ProseMirror document update // that has an analytics event. if (hasRequiredPerformanceAPIs && tr.docChanged && payload.action !== ACTION.INSERTED && payload.action !== ACTION.DELETED) { const measureName = `${payload.actionSubject}:${payload.action}:${payload.actionSubjectId}`; measureRender( // NOTE this name could be resulting in misleading data -- where if multiple payloads are // received before a render completes -- the measurement value will be inaccurate (this is // due to measureRender requiring unique measureNames) measureName, ({ duration, distortedDuration }) => { fireAnalyticsEvent(pluginState.createAnalyticsEvent)({ payload: extendPayload({ payload, duration, distortedDuration }), channel }); }); } } } } return pluginState; } } }); } /** * Analytics plugin to be added to an `EditorPresetBuilder` and used with `ComposableEditor` * from `@atlaskit/editor-core`. */ const analyticsPlugin = ({ config: options = {}, api }) => { var _api$featureFlags; const featureFlags = (api === null || api === void 0 ? void 0 : (_api$featureFlags = api.featureFlags) === null || _api$featureFlags === void 0 ? void 0 : _api$featureFlags.sharedState.currentState()) || {}; const analyticsEventPropQueue = new Set(); return { name: 'analytics', getSharedState: editorState => { var _analyticsPluginKey$g; if (!editorState) { return { createAnalyticsEvent: null, attachAnalyticsEvent: null, performanceTracking: undefined }; } const { createAnalyticsEvent, performanceTracking } = (_analyticsPluginKey$g = analyticsPluginKey.getState(editorState)) !== null && _analyticsPluginKey$g !== void 0 ? _analyticsPluginKey$g : {}; return { createAnalyticsEvent, attachAnalyticsEvent: createAttachPayloadIntoTransaction(editorState.selection), performanceTracking }; }, actions: { attachAnalyticsEvent: (payload, channel = editorAnalyticsChannel) => tr => { var _api$analytics$shared, _api$analytics; const { createAnalyticsEvent, attachAnalyticsEvent } = (_api$analytics$shared = api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.sharedState.currentState()) !== null && _api$analytics$shared !== void 0 ? _api$analytics$shared : {}; if (!tr || !createAnalyticsEvent || !attachAnalyticsEvent) { analyticsEventPropQueue.add({ payload, channel }); return false; } attachAnalyticsEvent({ tr, payload, channel }); return true; }, fireAnalyticsEvent(payload, channel = editorAnalyticsChannel, options) { var _api$analytics$shared2, _api$analytics2; const { createAnalyticsEvent } = (_api$analytics$shared2 = api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.sharedState.currentState()) !== null && _api$analytics$shared2 !== void 0 ? _api$analytics$shared2 : {}; if (options !== null && options !== void 0 && options.context) { payload = getStateContext(options.context.selection, payload); } if (!createAnalyticsEvent) { analyticsEventPropQueue.add({ payload, channel }); return; } // This cast is needed to satisfy TS when using custom payloads which is only allowed by // the fireAnalyticsEvent function in EditorAnalyticsAPI for now. // createAnalyticsEvent actually fires our standard AnalyticsEventPayload type which is less strict // so this is safe. const firedPayload = payload; fireAnalyticsEvent(createAnalyticsEvent, options)({ payload: firedPayload, channel }); } }, usePluginHook({ editorView }) { const { createAnalyticsEvent } = useAnalyticsEvents(); useLayoutEffect(() => { const { dispatch, state: { tr } } = editorView; tr.setMeta(analyticsPluginKey, { createAnalyticsEvent }); dispatch(tr); // Attach all analytics events to the transaction analyticsEventPropQueue.forEach(({ payload, channel }) => { var _createAnalyticsEvent; (_createAnalyticsEvent = createAnalyticsEvent(payload)) === null || _createAnalyticsEvent === void 0 ? void 0 : _createAnalyticsEvent.fire(channel !== null && channel !== void 0 ? channel : editorAnalyticsChannel); }); // Clear the queue analyticsEventPropQueue.clear(); }, [createAnalyticsEvent, editorView]); }, pmPlugins() { return [{ name: 'analyticsPlugin', plugin: () => createPlugin(options, featureFlags) }]; }, onEditorViewStateUpdated({ originalTransaction, transactions, newEditorState }) { const pluginState = analyticsPluginKey.getState(newEditorState); if (!pluginState || !pluginState.createAnalyticsEvent) { return; } // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any const steps = transactions.reduce((acc, tr) => { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any const payloads = tr.steps // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any .filter(step => step instanceof AnalyticsStep).map(x => x.analyticsEvents).reduce((acc, val) => acc.concat(val), []); acc.push(...payloads); return acc; }, []); if (steps.length === 0) { return; } const { createAnalyticsEvent } = pluginState; const undoAnaltyicsEventTransformer = generateUndoRedoInputSoucePayload(originalTransaction); steps.forEach(({ payload, channel }) => { const nextPayload = undoAnaltyicsEventTransformer(payload); fireAnalyticsEvent(createAnalyticsEvent)({ payload: nextPayload, channel }); }); } }; }; function extendPayload({ payload, duration, distortedDuration }) { return { ...payload, attributes: { ...payload.attributes, duration, distortedDuration }, eventType: EVENT_TYPE.OPERATIONAL }; } export { analyticsPlugin };