@atlaskit/editor-plugin-collab-edit 1.14.2 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @atlaskit/editor-plugin-collab-edit
2
2
 
3
+ ## 1.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#134153](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/134153)
8
+ [`88d5b34381594`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/88d5b34381594) -
9
+ Introduced filtering of spamming transaction
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies
14
+
15
+ ## 1.15.0
16
+
17
+ ### Minor Changes
18
+
19
+ - [#133504](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/133504)
20
+ [`024439741fcdc`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/024439741fcdc) -
21
+ Replaced usage of `originalTransactionHasMeta` to avoid infinite loop issue
22
+
3
23
  ## 1.14.2
4
24
 
5
25
  ### Patch Changes
@@ -18,6 +18,7 @@ var _sendTransaction = require("./events/send-transaction");
18
18
  var _main = require("./pm-plugins/main");
19
19
  var _pluginKey = require("./pm-plugins/main/plugin-key");
20
20
  var _nativeCollabProviderPlugin = require("./pm-plugins/native-collab-provider-plugin");
21
+ var _trackAndFilterSpammingSteps = require("./pm-plugins/track-and-filter-spamming-steps");
21
22
  var _trackLastOrganicChange = require("./pm-plugins/track-last-organic-change");
22
23
  var _trackNcsInitialization = require("./pm-plugins/track-ncs-initialization");
23
24
  var _trackSteps = require("./track-steps");
@@ -173,6 +174,27 @@ var collabEditPlugin = exports.collabEditPlugin = function collabEditPlugin(_ref
173
174
  name: 'collabTrackNCSInitializationPlugin',
174
175
  plugin: _trackNcsInitialization.createPlugin
175
176
  }]);
177
+ if ((0, _platformFeatureFlags.fg)('platform_editor_filter_transactions_analytics') || (0, _platformFeatureFlags.fg)('platform_editor_filter_spamming_transactions')) {
178
+ plugins.push({
179
+ name: 'trackAndFilterSpammingSteps',
180
+ plugin: function plugin() {
181
+ return (0, _trackAndFilterSpammingSteps.createPlugin)(function (tr) {
182
+ var _api$analytics;
183
+ var sanitizedSteps = tr.steps.map(function (step) {
184
+ return (0, _trackSteps.sanitizeStep)(step);
185
+ });
186
+ api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || (_api$analytics = _api$analytics.actions) === null || _api$analytics === void 0 || _api$analytics.fireAnalyticsEvent({
187
+ action: _analytics.ACTION.STEPS_FILTERED,
188
+ actionSubject: _analytics.ACTION_SUBJECT.COLLAB,
189
+ attributes: {
190
+ steps: sanitizedSteps
191
+ },
192
+ eventType: _analytics.EVENT_TYPE.OPERATIONAL
193
+ });
194
+ });
195
+ }
196
+ });
197
+ }
176
198
  if ((0, _platformFeatureFlags.fg)('platform_editor_last_organic_change')) {
177
199
  plugins.push({
178
200
  name: 'collabTrackLastOrganicChangePlugin',
@@ -182,8 +204,8 @@ var collabEditPlugin = exports.collabEditPlugin = function collabEditPlugin(_ref
182
204
  return plugins;
183
205
  },
184
206
  onEditorViewStateUpdated: function onEditorViewStateUpdated(props) {
185
- var _api$analytics, _api$editorViewMode, _options$useNativePlu;
186
- var addErrorAnalytics = (0, _analytics2.addSynchronyErrorAnalytics)(props.newEditorState, props.newEditorState.tr, featureFlags, api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions);
207
+ var _api$analytics2, _api$editorViewMode, _options$useNativePlu;
208
+ var addErrorAnalytics = (0, _analytics2.addSynchronyErrorAnalytics)(props.newEditorState, props.newEditorState.tr, featureFlags, api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions);
187
209
  var viewMode = api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode;
188
210
  executeProviderCode((0, _sendTransaction.sendTransaction)({
189
211
  originalTransaction: props.originalTransaction,
@@ -195,8 +217,8 @@ var collabEditPlugin = exports.collabEditPlugin = function collabEditPlugin(_ref
195
217
  }), addErrorAnalytics);
196
218
  (0, _trackSteps.track)(_objectSpread(_objectSpread({}, props), {}, {
197
219
  onTrackDataProcessed: function onTrackDataProcessed(steps) {
198
- var _api$analytics2;
199
- api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 || (_api$analytics2 = _api$analytics2.actions) === null || _api$analytics2 === void 0 || _api$analytics2.fireAnalyticsEvent({
220
+ var _api$analytics3;
221
+ api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 || (_api$analytics3 = _api$analytics3.actions) === null || _api$analytics3 === void 0 || _api$analytics3.fireAnalyticsEvent({
200
222
  action: _analytics.ACTION.STEPS_TRACKED,
201
223
  actionSubject: _analytics.ACTION_SUBJECT.COLLAB,
202
224
  attributes: {
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createPlugin = exports.createFilterTransaction = void 0;
7
+ exports.generateTransactionKey = generateTransactionKey;
8
+ exports.trackSpammingStepsPluginKey = void 0;
9
+ var _steps = require("@atlaskit/adf-schema/steps");
10
+ var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
11
+ var _state = require("@atlaskit/editor-prosemirror/state");
12
+ var _transform = require("@atlaskit/editor-prosemirror/transform");
13
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
14
+ var THRESHOLD = 50; // 50 milliseconds
15
+
16
+ var createFilterTransaction = exports.createFilterTransaction = function createFilterTransaction(recentTransactionsTimestamps, trackFilteredTransaction) {
17
+ return function (tr) {
18
+ if (Boolean(tr.getMeta('appendTransaction'))) {
19
+ return true;
20
+ }
21
+ var isRemote = Boolean(tr.getMeta('isRemote'));
22
+ if (isRemote) {
23
+ return true;
24
+ }
25
+ if (tr.docChanged && tr.doc.eq(tr.before)) {
26
+ var transactionKey = generateTransactionKey(tr);
27
+ if (!transactionKey) {
28
+ return true;
29
+ }
30
+
31
+ //Clean up tracked transactions when time over threshold
32
+ recentTransactionsTimestamps.forEach(function (value, key) {
33
+ if (tr.time - value.timestamp > THRESHOLD) {
34
+ // Delete tracked transaction when over threshold
35
+ recentTransactionsTimestamps.delete(key);
36
+ }
37
+ });
38
+ var lastTransactionEntry = recentTransactionsTimestamps.get(transactionKey);
39
+ if (!lastTransactionEntry) {
40
+ // If no timestamp exists for the given transaction, allow transaction and add an entry
41
+ recentTransactionsTimestamps.set(transactionKey, {
42
+ timestamp: tr.time,
43
+ steps: tr.steps
44
+ });
45
+ return true;
46
+ }
47
+
48
+ // Track analytics for the filtered transaction
49
+ trackFilteredTransaction(tr);
50
+ if ((0, _platformFeatureFlags.fg)('platform_editor_filter_spamming_transactions')) {
51
+ // Filter transaction
52
+ return false;
53
+ }
54
+ }
55
+ return true; // Allow the transaction
56
+ };
57
+ };
58
+
59
+ // Helper function to create a u ique transaction key
60
+ function generateTransactionKey(tr) {
61
+ return tr.steps.map(function (step) {
62
+ if (step instanceof _transform.RemoveNodeMarkStep || step instanceof _transform.AddNodeMarkStep || step instanceof _steps.SetAttrsStep || step instanceof _transform.AttrStep || step instanceof _steps.AnalyticsStep) {
63
+ if (step.pos !== undefined) {
64
+ return "".concat(step.pos);
65
+ }
66
+ } else if (step instanceof _transform.AddMarkStep || step instanceof _transform.RemoveMarkStep || step instanceof _transform.ReplaceAroundStep || step instanceof _transform.ReplaceStep) {
67
+ return "from_".concat(step.from, "_to_").concat(step.to);
68
+ } else if (step instanceof _steps.LinkMetaStep) {
69
+ return "from_".concat(step.toJSON().from, "_to_").concat(step.toJSON().to);
70
+ } else if (step instanceof _steps.InsertTypeAheadStep) {
71
+ return "insertTypeAheadStep";
72
+ }
73
+ return '';
74
+ }).join('_');
75
+ }
76
+ var trackSpammingStepsPluginKey = exports.trackSpammingStepsPluginKey = new _state.PluginKey('trackAndFilterSpammingStepsPluginKey');
77
+ var createPlugin = exports.createPlugin = function createPlugin(trackFilteredTransaction) {
78
+ var recentTransactionsTimestamps = new Map();
79
+ return new _safePlugin.SafePlugin({
80
+ key: trackSpammingStepsPluginKey,
81
+ filterTransaction: createFilterTransaction(recentTransactionsTimestamps, trackFilteredTransaction)
82
+ });
83
+ };
@@ -20,8 +20,11 @@ var createPlugin = exports.createPlugin = function createPlugin() {
20
20
  };
21
21
  },
22
22
  apply: function apply(transaction, prevPluginState) {
23
- var isRemote = (0, _utils.originalTransactionHasMeta)(transaction, 'isRemote');
24
- var isDocumentReplaceFromRemote = isRemote && (0, _utils.originalTransactionHasMeta)(transaction, 'replaceDocument');
23
+ if (Boolean(transaction.getMeta('appendedTransaction'))) {
24
+ return prevPluginState;
25
+ }
26
+ var isRemote = Boolean(transaction.getMeta('isRemote'));
27
+ var isDocumentReplaceFromRemote = isRemote && Boolean(transaction.getMeta('replaceDocument'));
25
28
  if (isDocumentReplaceFromRemote) {
26
29
  return prevPluginState;
27
30
  }
@@ -7,9 +7,10 @@ import { sendTransaction } from './events/send-transaction';
7
7
  import { createPlugin } from './pm-plugins/main';
8
8
  import { pluginKey as mainPluginKey } from './pm-plugins/main/plugin-key';
9
9
  import { nativeCollabProviderPlugin } from './pm-plugins/native-collab-provider-plugin';
10
+ import { createPlugin as trackSpammingStepsPlugin } from './pm-plugins/track-and-filter-spamming-steps';
10
11
  import { createPlugin as createLastOrganicChangePlugin, trackLastOrganicChangePluginKey } from './pm-plugins/track-last-organic-change';
11
12
  import { createPlugin as createTrackNCSInitializationPlugin, trackNCSInitializationPluginKey } from './pm-plugins/track-ncs-initialization';
12
- import { track } from './track-steps';
13
+ import { sanitizeStep, track } from './track-steps';
13
14
  import { getAvatarColor } from './utils';
14
15
  const providerBuilder = collabEditProviderPromise => async (codeToExecute, onError) => {
15
16
  try {
@@ -128,6 +129,23 @@ export const collabEditPlugin = ({
128
129
  name: 'collabTrackNCSInitializationPlugin',
129
130
  plugin: createTrackNCSInitializationPlugin
130
131
  }];
132
+ if (fg('platform_editor_filter_transactions_analytics') || fg('platform_editor_filter_spamming_transactions')) {
133
+ plugins.push({
134
+ name: 'trackAndFilterSpammingSteps',
135
+ plugin: () => trackSpammingStepsPlugin(tr => {
136
+ var _api$analytics, _api$analytics$action;
137
+ const sanitizedSteps = tr.steps.map(step => sanitizeStep(step));
138
+ 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({
139
+ action: ACTION.STEPS_FILTERED,
140
+ actionSubject: ACTION_SUBJECT.COLLAB,
141
+ attributes: {
142
+ steps: sanitizedSteps
143
+ },
144
+ eventType: EVENT_TYPE.OPERATIONAL
145
+ });
146
+ })
147
+ });
148
+ }
131
149
  if (fg('platform_editor_last_organic_change')) {
132
150
  plugins.push({
133
151
  name: 'collabTrackLastOrganicChangePlugin',
@@ -137,8 +155,8 @@ export const collabEditPlugin = ({
137
155
  return plugins;
138
156
  },
139
157
  onEditorViewStateUpdated(props) {
140
- var _api$analytics, _api$editorViewMode, _api$editorViewMode$s, _options$useNativePlu;
141
- const addErrorAnalytics = addSynchronyErrorAnalytics(props.newEditorState, props.newEditorState.tr, featureFlags, api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions);
158
+ var _api$analytics2, _api$editorViewMode, _api$editorViewMode$s, _options$useNativePlu;
159
+ const addErrorAnalytics = addSynchronyErrorAnalytics(props.newEditorState, props.newEditorState.tr, featureFlags, api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions);
142
160
  const viewMode = api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : (_api$editorViewMode$s = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode$s === void 0 ? void 0 : _api$editorViewMode$s.mode;
143
161
  executeProviderCode(sendTransaction({
144
162
  originalTransaction: props.originalTransaction,
@@ -151,8 +169,8 @@ export const collabEditPlugin = ({
151
169
  track({
152
170
  ...props,
153
171
  onTrackDataProcessed: steps => {
154
- var _api$analytics2, _api$analytics2$actio;
155
- api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : (_api$analytics2$actio = _api$analytics2.actions) === null || _api$analytics2$actio === void 0 ? void 0 : _api$analytics2$actio.fireAnalyticsEvent({
172
+ var _api$analytics3, _api$analytics3$actio;
173
+ api === null || api === void 0 ? void 0 : (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : (_api$analytics3$actio = _api$analytics3.actions) === null || _api$analytics3$actio === void 0 ? void 0 : _api$analytics3$actio.fireAnalyticsEvent({
156
174
  action: ACTION.STEPS_TRACKED,
157
175
  actionSubject: ACTION_SUBJECT.COLLAB,
158
176
  attributes: {
@@ -0,0 +1,75 @@
1
+ import { AnalyticsStep, InsertTypeAheadStep, LinkMetaStep, SetAttrsStep } from '@atlaskit/adf-schema/steps';
2
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
3
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
4
+ import { AddMarkStep, AddNodeMarkStep, AttrStep, RemoveMarkStep, RemoveNodeMarkStep, ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
5
+ import { fg } from '@atlaskit/platform-feature-flags';
6
+ const THRESHOLD = 50; // 50 milliseconds
7
+
8
+ export const createFilterTransaction = (recentTransactionsTimestamps, trackFilteredTransaction) => {
9
+ return tr => {
10
+ if (Boolean(tr.getMeta('appendTransaction'))) {
11
+ return true;
12
+ }
13
+ const isRemote = Boolean(tr.getMeta('isRemote'));
14
+ if (isRemote) {
15
+ return true;
16
+ }
17
+ if (tr.docChanged && tr.doc.eq(tr.before)) {
18
+ const transactionKey = generateTransactionKey(tr);
19
+ if (!transactionKey) {
20
+ return true;
21
+ }
22
+
23
+ //Clean up tracked transactions when time over threshold
24
+ recentTransactionsTimestamps.forEach((value, key) => {
25
+ if (tr.time - value.timestamp > THRESHOLD) {
26
+ // Delete tracked transaction when over threshold
27
+ recentTransactionsTimestamps.delete(key);
28
+ }
29
+ });
30
+ const lastTransactionEntry = recentTransactionsTimestamps.get(transactionKey);
31
+ if (!lastTransactionEntry) {
32
+ // If no timestamp exists for the given transaction, allow transaction and add an entry
33
+ recentTransactionsTimestamps.set(transactionKey, {
34
+ timestamp: tr.time,
35
+ steps: tr.steps
36
+ });
37
+ return true;
38
+ }
39
+
40
+ // Track analytics for the filtered transaction
41
+ trackFilteredTransaction(tr);
42
+ if (fg('platform_editor_filter_spamming_transactions')) {
43
+ // Filter transaction
44
+ return false;
45
+ }
46
+ }
47
+ return true; // Allow the transaction
48
+ };
49
+ };
50
+
51
+ // Helper function to create a u ique transaction key
52
+ export function generateTransactionKey(tr) {
53
+ return tr.steps.map(step => {
54
+ if (step instanceof RemoveNodeMarkStep || step instanceof AddNodeMarkStep || step instanceof SetAttrsStep || step instanceof AttrStep || step instanceof AnalyticsStep) {
55
+ if (step.pos !== undefined) {
56
+ return `${step.pos}`;
57
+ }
58
+ } else if (step instanceof AddMarkStep || step instanceof RemoveMarkStep || step instanceof ReplaceAroundStep || step instanceof ReplaceStep) {
59
+ return `from_${step.from}_to_${step.to}`;
60
+ } else if (step instanceof LinkMetaStep) {
61
+ return `from_${step.toJSON().from}_to_${step.toJSON().to}`;
62
+ } else if (step instanceof InsertTypeAheadStep) {
63
+ return `insertTypeAheadStep`;
64
+ }
65
+ return '';
66
+ }).join('_');
67
+ }
68
+ export const trackSpammingStepsPluginKey = new PluginKey('trackAndFilterSpammingStepsPluginKey');
69
+ export const createPlugin = trackFilteredTransaction => {
70
+ const recentTransactionsTimestamps = new Map();
71
+ return new SafePlugin({
72
+ key: trackSpammingStepsPluginKey,
73
+ filterTransaction: createFilterTransaction(recentTransactionsTimestamps, trackFilteredTransaction)
74
+ });
75
+ };
@@ -1,7 +1,7 @@
1
1
  import { isDirtyTransaction } from '@atlaskit/editor-common/collab';
2
2
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
3
3
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
4
- import { isOrganicChange, originalTransactionHasMeta } from '../utils';
4
+ import { isOrganicChange } from '../utils';
5
5
  export const trackLastOrganicChangePluginKey = new PluginKey('collabTrackLastOrganicChangePlugin');
6
6
  export const createPlugin = () => {
7
7
  return new SafePlugin({
@@ -14,8 +14,11 @@ export const createPlugin = () => {
14
14
  };
15
15
  },
16
16
  apply(transaction, prevPluginState) {
17
- const isRemote = originalTransactionHasMeta(transaction, 'isRemote');
18
- const isDocumentReplaceFromRemote = isRemote && originalTransactionHasMeta(transaction, 'replaceDocument');
17
+ if (Boolean(transaction.getMeta('appendedTransaction'))) {
18
+ return prevPluginState;
19
+ }
20
+ const isRemote = Boolean(transaction.getMeta('isRemote'));
21
+ const isDocumentReplaceFromRemote = isRemote && Boolean(transaction.getMeta('replaceDocument'));
19
22
  if (isDocumentReplaceFromRemote) {
20
23
  return prevPluginState;
21
24
  }
@@ -13,9 +13,10 @@ import { sendTransaction } from './events/send-transaction';
13
13
  import { createPlugin } from './pm-plugins/main';
14
14
  import { pluginKey as mainPluginKey } from './pm-plugins/main/plugin-key';
15
15
  import { nativeCollabProviderPlugin } from './pm-plugins/native-collab-provider-plugin';
16
+ import { createPlugin as trackSpammingStepsPlugin } from './pm-plugins/track-and-filter-spamming-steps';
16
17
  import { createPlugin as createLastOrganicChangePlugin, trackLastOrganicChangePluginKey } from './pm-plugins/track-last-organic-change';
17
18
  import { createPlugin as createTrackNCSInitializationPlugin, trackNCSInitializationPluginKey } from './pm-plugins/track-ncs-initialization';
18
- import { track } from './track-steps';
19
+ import { sanitizeStep, track } from './track-steps';
19
20
  import { getAvatarColor } from './utils';
20
21
  var providerBuilder = function providerBuilder(collabEditProviderPromise) {
21
22
  return /*#__PURE__*/function () {
@@ -166,6 +167,27 @@ export var collabEditPlugin = function collabEditPlugin(_ref4) {
166
167
  name: 'collabTrackNCSInitializationPlugin',
167
168
  plugin: createTrackNCSInitializationPlugin
168
169
  }]);
170
+ if (fg('platform_editor_filter_transactions_analytics') || fg('platform_editor_filter_spamming_transactions')) {
171
+ plugins.push({
172
+ name: 'trackAndFilterSpammingSteps',
173
+ plugin: function plugin() {
174
+ return trackSpammingStepsPlugin(function (tr) {
175
+ var _api$analytics;
176
+ var sanitizedSteps = tr.steps.map(function (step) {
177
+ return sanitizeStep(step);
178
+ });
179
+ api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || (_api$analytics = _api$analytics.actions) === null || _api$analytics === void 0 || _api$analytics.fireAnalyticsEvent({
180
+ action: ACTION.STEPS_FILTERED,
181
+ actionSubject: ACTION_SUBJECT.COLLAB,
182
+ attributes: {
183
+ steps: sanitizedSteps
184
+ },
185
+ eventType: EVENT_TYPE.OPERATIONAL
186
+ });
187
+ });
188
+ }
189
+ });
190
+ }
169
191
  if (fg('platform_editor_last_organic_change')) {
170
192
  plugins.push({
171
193
  name: 'collabTrackLastOrganicChangePlugin',
@@ -175,8 +197,8 @@ export var collabEditPlugin = function collabEditPlugin(_ref4) {
175
197
  return plugins;
176
198
  },
177
199
  onEditorViewStateUpdated: function onEditorViewStateUpdated(props) {
178
- var _api$analytics, _api$editorViewMode, _options$useNativePlu;
179
- var addErrorAnalytics = addSynchronyErrorAnalytics(props.newEditorState, props.newEditorState.tr, featureFlags, api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions);
200
+ var _api$analytics2, _api$editorViewMode, _options$useNativePlu;
201
+ var addErrorAnalytics = addSynchronyErrorAnalytics(props.newEditorState, props.newEditorState.tr, featureFlags, api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions);
180
202
  var viewMode = api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode;
181
203
  executeProviderCode(sendTransaction({
182
204
  originalTransaction: props.originalTransaction,
@@ -188,8 +210,8 @@ export var collabEditPlugin = function collabEditPlugin(_ref4) {
188
210
  }), addErrorAnalytics);
189
211
  track(_objectSpread(_objectSpread({}, props), {}, {
190
212
  onTrackDataProcessed: function onTrackDataProcessed(steps) {
191
- var _api$analytics2;
192
- api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 || (_api$analytics2 = _api$analytics2.actions) === null || _api$analytics2 === void 0 || _api$analytics2.fireAnalyticsEvent({
213
+ var _api$analytics3;
214
+ api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 || (_api$analytics3 = _api$analytics3.actions) === null || _api$analytics3 === void 0 || _api$analytics3.fireAnalyticsEvent({
193
215
  action: ACTION.STEPS_TRACKED,
194
216
  actionSubject: ACTION_SUBJECT.COLLAB,
195
217
  attributes: {
@@ -0,0 +1,75 @@
1
+ import { AnalyticsStep, InsertTypeAheadStep, LinkMetaStep, SetAttrsStep } from '@atlaskit/adf-schema/steps';
2
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
3
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
4
+ import { AddMarkStep, AddNodeMarkStep, AttrStep, RemoveMarkStep, RemoveNodeMarkStep, ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
5
+ import { fg } from '@atlaskit/platform-feature-flags';
6
+ var THRESHOLD = 50; // 50 milliseconds
7
+
8
+ export var createFilterTransaction = function createFilterTransaction(recentTransactionsTimestamps, trackFilteredTransaction) {
9
+ return function (tr) {
10
+ if (Boolean(tr.getMeta('appendTransaction'))) {
11
+ return true;
12
+ }
13
+ var isRemote = Boolean(tr.getMeta('isRemote'));
14
+ if (isRemote) {
15
+ return true;
16
+ }
17
+ if (tr.docChanged && tr.doc.eq(tr.before)) {
18
+ var transactionKey = generateTransactionKey(tr);
19
+ if (!transactionKey) {
20
+ return true;
21
+ }
22
+
23
+ //Clean up tracked transactions when time over threshold
24
+ recentTransactionsTimestamps.forEach(function (value, key) {
25
+ if (tr.time - value.timestamp > THRESHOLD) {
26
+ // Delete tracked transaction when over threshold
27
+ recentTransactionsTimestamps.delete(key);
28
+ }
29
+ });
30
+ var lastTransactionEntry = recentTransactionsTimestamps.get(transactionKey);
31
+ if (!lastTransactionEntry) {
32
+ // If no timestamp exists for the given transaction, allow transaction and add an entry
33
+ recentTransactionsTimestamps.set(transactionKey, {
34
+ timestamp: tr.time,
35
+ steps: tr.steps
36
+ });
37
+ return true;
38
+ }
39
+
40
+ // Track analytics for the filtered transaction
41
+ trackFilteredTransaction(tr);
42
+ if (fg('platform_editor_filter_spamming_transactions')) {
43
+ // Filter transaction
44
+ return false;
45
+ }
46
+ }
47
+ return true; // Allow the transaction
48
+ };
49
+ };
50
+
51
+ // Helper function to create a u ique transaction key
52
+ export function generateTransactionKey(tr) {
53
+ return tr.steps.map(function (step) {
54
+ if (step instanceof RemoveNodeMarkStep || step instanceof AddNodeMarkStep || step instanceof SetAttrsStep || step instanceof AttrStep || step instanceof AnalyticsStep) {
55
+ if (step.pos !== undefined) {
56
+ return "".concat(step.pos);
57
+ }
58
+ } else if (step instanceof AddMarkStep || step instanceof RemoveMarkStep || step instanceof ReplaceAroundStep || step instanceof ReplaceStep) {
59
+ return "from_".concat(step.from, "_to_").concat(step.to);
60
+ } else if (step instanceof LinkMetaStep) {
61
+ return "from_".concat(step.toJSON().from, "_to_").concat(step.toJSON().to);
62
+ } else if (step instanceof InsertTypeAheadStep) {
63
+ return "insertTypeAheadStep";
64
+ }
65
+ return '';
66
+ }).join('_');
67
+ }
68
+ export var trackSpammingStepsPluginKey = new PluginKey('trackAndFilterSpammingStepsPluginKey');
69
+ export var createPlugin = function createPlugin(trackFilteredTransaction) {
70
+ var recentTransactionsTimestamps = new Map();
71
+ return new SafePlugin({
72
+ key: trackSpammingStepsPluginKey,
73
+ filterTransaction: createFilterTransaction(recentTransactionsTimestamps, trackFilteredTransaction)
74
+ });
75
+ };
@@ -1,7 +1,7 @@
1
1
  import { isDirtyTransaction } from '@atlaskit/editor-common/collab';
2
2
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
3
3
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
4
- import { isOrganicChange, originalTransactionHasMeta } from '../utils';
4
+ import { isOrganicChange } from '../utils';
5
5
  export var trackLastOrganicChangePluginKey = new PluginKey('collabTrackLastOrganicChangePlugin');
6
6
  export var createPlugin = function createPlugin() {
7
7
  return new SafePlugin({
@@ -14,8 +14,11 @@ export var createPlugin = function createPlugin() {
14
14
  };
15
15
  },
16
16
  apply: function apply(transaction, prevPluginState) {
17
- var isRemote = originalTransactionHasMeta(transaction, 'isRemote');
18
- var isDocumentReplaceFromRemote = isRemote && originalTransactionHasMeta(transaction, 'replaceDocument');
17
+ if (Boolean(transaction.getMeta('appendedTransaction'))) {
18
+ return prevPluginState;
19
+ }
20
+ var isRemote = Boolean(transaction.getMeta('isRemote'));
21
+ var isDocumentReplaceFromRemote = isRemote && Boolean(transaction.getMeta('replaceDocument'));
19
22
  if (isDocumentReplaceFromRemote) {
20
23
  return prevPluginState;
21
24
  }
@@ -0,0 +1,14 @@
1
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
2
+ import { PluginKey, type Transaction } from '@atlaskit/editor-prosemirror/state';
3
+ import { type Step } from '@atlaskit/editor-prosemirror/transform';
4
+ import type { TrackSpammingStepsMetadata } from '../types';
5
+ export type RecentTransactionTimestamp = {
6
+ timestamp: number;
7
+ steps: Step[];
8
+ };
9
+ export type RecentTransactionTimestamps = Map<string, RecentTransactionTimestamp>;
10
+ export type TrackFilteredTransaction = (tr: Transaction) => void;
11
+ export declare const createFilterTransaction: (recentTransactionsTimestamps: RecentTransactionTimestamps, trackFilteredTransaction: TrackFilteredTransaction) => (tr: Transaction) => boolean;
12
+ export declare function generateTransactionKey(tr: Transaction): string;
13
+ export declare const trackSpammingStepsPluginKey: PluginKey<TrackSpammingStepsMetadata>;
14
+ export declare const createPlugin: (trackFilteredTransaction: TrackFilteredTransaction) => SafePlugin<TrackSpammingStepsMetadata>;
@@ -3,6 +3,7 @@ import type { NextEditorPlugin, OptionalPlugin } from '@atlaskit/editor-common/t
3
3
  import type { AnalyticsPlugin } from '@atlaskit/editor-plugin-analytics';
4
4
  import type { EditorViewModePlugin } from '@atlaskit/editor-plugin-editor-viewmode';
5
5
  import type { FeatureFlagsPlugin } from '@atlaskit/editor-plugin-feature-flags';
6
+ import { type Step } from '@atlaskit/editor-prosemirror/dist/types/transform';
6
7
  import type { Mark } from '@atlaskit/editor-prosemirror/model';
7
8
  import type { Transaction } from '@atlaskit/editor-prosemirror/state';
8
9
  export type { InviteToEditComponentProps, InviteToEditButtonProps, CollabInviteToEditProps, CollabAnalyticsProps, } from '@atlaskit/editor-common/collab';
@@ -26,6 +27,12 @@ export type LastOrganicChangeMetadata = {
26
27
  lastLocalOrganicChangeAt: null | number;
27
28
  lastRemoteOrganicChangeAt: null | number;
28
29
  };
30
+ export type TrackSpammingStepsMetadata = {
31
+ recentTransactionsTimestemps: Map<string, {
32
+ timestamp: number;
33
+ steps: Step[];
34
+ }>;
35
+ };
29
36
  export type CollabEditPluginSharedState = {
30
37
  initialised: CollabInitializedMetadata & LastOrganicChangeMetadata;
31
38
  activeParticipants: ReadOnlyParticipants | undefined;
@@ -0,0 +1,14 @@
1
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
2
+ import { PluginKey, type Transaction } from '@atlaskit/editor-prosemirror/state';
3
+ import { type Step } from '@atlaskit/editor-prosemirror/transform';
4
+ import type { TrackSpammingStepsMetadata } from '../types';
5
+ export type RecentTransactionTimestamp = {
6
+ timestamp: number;
7
+ steps: Step[];
8
+ };
9
+ export type RecentTransactionTimestamps = Map<string, RecentTransactionTimestamp>;
10
+ export type TrackFilteredTransaction = (tr: Transaction) => void;
11
+ export declare const createFilterTransaction: (recentTransactionsTimestamps: RecentTransactionTimestamps, trackFilteredTransaction: TrackFilteredTransaction) => (tr: Transaction) => boolean;
12
+ export declare function generateTransactionKey(tr: Transaction): string;
13
+ export declare const trackSpammingStepsPluginKey: PluginKey<TrackSpammingStepsMetadata>;
14
+ export declare const createPlugin: (trackFilteredTransaction: TrackFilteredTransaction) => SafePlugin<TrackSpammingStepsMetadata>;
@@ -3,6 +3,7 @@ import type { NextEditorPlugin, OptionalPlugin } from '@atlaskit/editor-common/t
3
3
  import type { AnalyticsPlugin } from '@atlaskit/editor-plugin-analytics';
4
4
  import type { EditorViewModePlugin } from '@atlaskit/editor-plugin-editor-viewmode';
5
5
  import type { FeatureFlagsPlugin } from '@atlaskit/editor-plugin-feature-flags';
6
+ import { type Step } from '@atlaskit/editor-prosemirror/dist/types/transform';
6
7
  import type { Mark } from '@atlaskit/editor-prosemirror/model';
7
8
  import type { Transaction } from '@atlaskit/editor-prosemirror/state';
8
9
  export type { InviteToEditComponentProps, InviteToEditButtonProps, CollabInviteToEditProps, CollabAnalyticsProps, } from '@atlaskit/editor-common/collab';
@@ -26,6 +27,12 @@ export type LastOrganicChangeMetadata = {
26
27
  lastLocalOrganicChangeAt: null | number;
27
28
  lastRemoteOrganicChangeAt: null | number;
28
29
  };
30
+ export type TrackSpammingStepsMetadata = {
31
+ recentTransactionsTimestemps: Map<string, {
32
+ timestamp: number;
33
+ steps: Step[];
34
+ }>;
35
+ };
29
36
  export type CollabEditPluginSharedState = {
30
37
  initialised: CollabInitializedMetadata & LastOrganicChangeMetadata;
31
38
  activeParticipants: ReadOnlyParticipants | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-collab-edit",
3
- "version": "1.14.2",
3
+ "version": "1.16.0",
4
4
  "description": "Collab Edit plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "@atlaskit/adf-schema": "^40.8.1",
36
36
  "@atlaskit/custom-steps": "^0.7.0",
37
- "@atlaskit/editor-common": "^88.0.0",
37
+ "@atlaskit/editor-common": "^88.2.0",
38
38
  "@atlaskit/editor-plugin-analytics": "1.8.1",
39
39
  "@atlaskit/editor-plugin-editor-viewmode": "^2.1.0",
40
40
  "@atlaskit/editor-plugin-feature-flags": "^1.2.0",
@@ -51,8 +51,8 @@
51
51
  "devDependencies": {
52
52
  "@af/integration-testing": "*",
53
53
  "@af/visual-regression": "*",
54
- "@atlaskit/editor-plugin-mentions": "^2.4.0",
55
- "@atlaskit/editor-plugin-text-formatting": "^1.13.0",
54
+ "@atlaskit/editor-plugin-mentions": "^2.5.0",
55
+ "@atlaskit/editor-plugin-text-formatting": "^1.14.0",
56
56
  "@atlaskit/editor-plugin-type-ahead": "^1.8.0",
57
57
  "@atlaskit/editor-plugin-unsupported-content": "^1.8.0",
58
58
  "@atlaskit/editor-test-helpers": "^18.31.0",
@@ -102,6 +102,12 @@
102
102
  "platform-feature-flags": {
103
103
  "platform_editor_last_organic_change": {
104
104
  "type": "boolean"
105
+ },
106
+ "platform_editor_filter_transactions_analytics": {
107
+ "type": "boolean"
108
+ },
109
+ "platform_editor_filter_spamming_transactions": {
110
+ "type": "boolean"
105
111
  }
106
112
  }
107
113
  }