@atlaskit/editor-plugin-analytics
Version:
Analytics plugin for @atlaskit/editor-core
253 lines (247 loc) • 9.59 kB
JavaScript
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 };