@atlaskit/editor-plugin-collab-edit 7.2.0 → 7.2.1

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,15 @@
1
1
  # @atlaskit/editor-plugin-collab-edit
2
2
 
3
+ ## 7.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`c32dc3155a31a`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/c32dc3155a31a) -
8
+ Added an organic changes monitor which will reported anayltics when an organic change occurs. The
9
+ new analytic will contain extra information regarding only the transaction which caused the
10
+ organic change.
11
+ - Updated dependencies
12
+
3
13
  ## 7.2.0
4
14
 
5
15
  ### Minor Changes
@@ -13,6 +13,7 @@ var _analytics = require("@atlaskit/editor-common/analytics");
13
13
  var _utils = require("@atlaskit/editor-common/utils");
14
14
  var _editorJsonTransformer = require("@atlaskit/editor-json-transformer");
15
15
  var _transform = require("@atlaskit/editor-prosemirror/transform");
16
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
16
17
  var _prosemirrorCollab = require("@atlaskit/prosemirror-collab");
17
18
  var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
18
19
  var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
@@ -22,6 +23,7 @@ var _filterAnalytics = require("./pm-plugins/filterAnalytics");
22
23
  var _main = require("./pm-plugins/main");
23
24
  var _pluginKey = require("./pm-plugins/main/plugin-key");
24
25
  var _mergeUnconfirmed = require("./pm-plugins/mergeUnconfirmed");
26
+ var _monitorOrganicChanges = require("./pm-plugins/monitor-organic-changes");
25
27
  var _nativeCollabProviderPlugin = require("./pm-plugins/native-collab-provider-plugin");
26
28
  var _trackAndFilterSpammingSteps = require("./pm-plugins/track-and-filter-spamming-steps");
27
29
  var _trackLastOrganicChange = require("./pm-plugins/track-last-organic-change");
@@ -190,7 +192,7 @@ var collabEditPlugin = exports.collabEditPlugin = function collabEditPlugin(_ref
190
192
  return content.every(function (node) {
191
193
  try {
192
194
  node.check(); // this will throw an error if the node is invalid
193
- } catch (error) {
195
+ } catch (_unused) {
194
196
  return false;
195
197
  }
196
198
  return true;
@@ -301,6 +303,23 @@ var collabEditPlugin = exports.collabEditPlugin = function collabEditPlugin(_ref
301
303
  });
302
304
  }
303
305
  }));
306
+ if ((0, _platformFeatureFlags.fg)('platform_editor_collab_organic_change_reporting')) {
307
+ (0, _monitorOrganicChanges.monitorOrganic)(_objectSpread(_objectSpread({
308
+ api: api
309
+ }, props), {}, {
310
+ onDataProcessed: function onDataProcessed(data) {
311
+ var _api$analytics4;
312
+ api === null || api === void 0 || (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 || (_api$analytics4 = _api$analytics4.actions) === null || _api$analytics4 === void 0 || _api$analytics4.fireAnalyticsEvent({
313
+ action: _analytics.ACTION.ORGANIC_CHANGES_TRACKED,
314
+ actionSubject: _analytics.ACTION_SUBJECT.COLLAB,
315
+ attributes: {
316
+ organicChanges: data
317
+ },
318
+ eventType: _analytics.EVENT_TYPE.OPERATIONAL
319
+ });
320
+ }
321
+ }));
322
+ }
304
323
  },
305
324
  commands: {
306
325
  nudgeTelepointer: function nudgeTelepointer(sessionId) {
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.monitorOrganic = exports.getScheduler = void 0;
8
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
+ var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
10
+ var _steps = require("@atlaskit/adf-schema/steps");
11
+ var _trackLastOrganicChange = require("./track-last-organic-change");
12
+ var _trackSteps = require("./track-steps");
13
+ var _excluded = ["steps"];
14
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
15
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
16
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
17
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
18
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
19
+ // This is essentially a queue of cached events. When the background task runs to send these items then this queue is flushed.
20
+ var organicReportingCache = [];
21
+ // Every ten seconds we will try to process the step data.
22
+ var LOW_PRIORITY_DELAY = 10000;
23
+
24
+ // See https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/
25
+
26
+ var getScheduler = exports.getScheduler = function getScheduler(obj) {
27
+ if (!obj) {
28
+ return null;
29
+ }
30
+ if ('scheduler' in obj) {
31
+ return obj.scheduler;
32
+ }
33
+ return null;
34
+ };
35
+
36
+ /**
37
+ * Processes the steps metadata from the cache and calls the callback function with the processed data.
38
+ *
39
+ * @param {OrganicCacheType} cache - A cache containing steps metadata.
40
+ * @param {(data: OrganicMetadataAnalytics[]) => void} onDataProcessed - Callback function to be called with the processed data.
41
+ */
42
+ var task = function task(cache, onDataProcessed) {
43
+ var data = [];
44
+ var _iterator = _createForOfIteratorHelper(cache),
45
+ _step;
46
+ try {
47
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
48
+ var item = _step.value;
49
+ var steps = item.steps,
50
+ rest = (0, _objectWithoutProperties2.default)(item, _excluded);
51
+ // We'll use the same grouping and sanitize logic from the track-steps util
52
+ var stepTypesAmount = (0, _trackSteps.groupSteps)(steps.map(_trackSteps.sanitizeStep));
53
+ data.push(_objectSpread(_objectSpread({}, rest), {}, {
54
+ stepTypesAmount: stepTypesAmount
55
+ }));
56
+ }
57
+
58
+ // clear the cache.
59
+ } catch (err) {
60
+ _iterator.e(err);
61
+ } finally {
62
+ _iterator.f();
63
+ }
64
+ cache.length = 0;
65
+ if (data.length > 0) {
66
+ onDataProcessed(data);
67
+ }
68
+ };
69
+
70
+ /**
71
+ * Tracks the steps sent by the client by storing them in a cache and scheduling a task to process them. Once the steps are processed, the onDataProcessed callabck will be called.
72
+ *
73
+ * This is a non-critical code. If the browser doesn't support the Scheduler API https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/
74
+ *
75
+ * @param {TrackProps} props - The properties required for tracking steps.
76
+ * @param {ExtractInjectionAPI<CollabEditPlugin> | undefined} props.api - The API for the CollabEdit plugin.
77
+ * @param {EditorState} props.newEditorState - The new editor state.
78
+ * @param {Readonly<Transaction[]>} props.transactions - The transactions that contain the steps.
79
+ * @param {(data: OrganicMetadataAnalytics[]) => void} props.onDataProcessed - Callback function to be called with the processed data.
80
+ */
81
+ var monitorOrganic = exports.monitorOrganic = function monitorOrganic(_ref) {
82
+ var newEditorState = _ref.newEditorState,
83
+ oldEditorState = _ref.oldEditorState,
84
+ transactions = _ref.transactions,
85
+ onDataProcessed = _ref.onDataProcessed;
86
+ // We can exclude analytic steps since they should never trigger an organic change.
87
+ var newSteps = transactions.flatMap(function (t) {
88
+ return t.steps;
89
+ }).filter(function (step) {
90
+ return !(step instanceof _steps.AnalyticsStep);
91
+ });
92
+ var scheduler = getScheduler(window);
93
+ if (!newSteps.length || !scheduler) {
94
+ return;
95
+ }
96
+
97
+ // We know that an organic change during startup will trigger a draft sync which will;
98
+ // fire editor edited event -> confluence/next/packages/editor-features/src/hooks/useDraftSync/useDraftSync.tsx
99
+ // and call triggerUpdate() notifying the BE that user edited the page -> confluence/next/packages/editor-features/src/hooks/useDraftSync/useEditorDraftSyncAction.tsx
100
+ // This can cause a problem with statsig metrics if organic changes are being incorrectly reported, ie an automated change
101
+ // occurs which contributes the user towards editing a page when in fact they didn't edit the page.
102
+ //
103
+ var oldPluginState = _trackLastOrganicChange.trackLastOrganicChangePluginKey.getState(oldEditorState);
104
+ var newPluginState = _trackLastOrganicChange.trackLastOrganicChangePluginKey.getState(newEditorState);
105
+ if (newSteps.length && (oldPluginState === null || oldPluginState === void 0 ? void 0 : oldPluginState.lastLocalOrganicBodyChangeAt) !== (newPluginState === null || newPluginState === void 0 ? void 0 : newPluginState.lastLocalOrganicBodyChangeAt)) {
106
+ var now = Date.now();
107
+ var isFirstChange = !(oldPluginState !== null && oldPluginState !== void 0 && oldPluginState.lastLocalOrganicBodyChangeAt);
108
+
109
+ // Check if we should compact with the previous entry (within 2 seconds) this means with a possible 10sec delay
110
+ // due to the postTask we could have a potential 5 grouped organic changes listed.
111
+ var shouldCompact = organicReportingCache.length > 0 && now - organicReportingCache[organicReportingCache.length - 1].startedAt < 2000;
112
+ if (shouldCompact) {
113
+ // Compact with previous entry
114
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
115
+ var prev = organicReportingCache.pop(); // We know it exists due to shouldCompact check
116
+
117
+ // tr.docChanged
118
+ organicReportingCache.push({
119
+ transactions: prev.transactions + 1,
120
+ startedAt: prev.startedAt,
121
+ endedAt: now,
122
+ isFirstChange: prev.isFirstChange || isFirstChange,
123
+ steps: prev.steps.concat(newSteps)
124
+ });
125
+ } else {
126
+ // Add new entry
127
+ organicReportingCache.push({
128
+ transactions: 1,
129
+ startedAt: now,
130
+ endedAt: now,
131
+ isFirstChange: isFirstChange,
132
+ steps: newSteps
133
+ });
134
+ }
135
+ if (organicReportingCache.length === 1) {
136
+ scheduler.postTask(function () {
137
+ task(organicReportingCache, onDataProcessed);
138
+ }, {
139
+ priority: 'background',
140
+ delay: LOW_PRIORITY_DELAY
141
+ });
142
+ }
143
+ }
144
+ };
@@ -2,6 +2,7 @@ import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/anal
2
2
  import { isEmptyDocument } from '@atlaskit/editor-common/utils';
3
3
  import { JSONTransformer } from '@atlaskit/editor-json-transformer';
4
4
  import { AddMarkStep, AddNodeMarkStep } from '@atlaskit/editor-prosemirror/transform';
5
+ import { fg } from '@atlaskit/platform-feature-flags';
5
6
  import { collab, getCollabState, sendableSteps } from '@atlaskit/prosemirror-collab';
6
7
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
7
8
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
@@ -11,6 +12,7 @@ import { filterAnalyticsSteps } from './pm-plugins/filterAnalytics';
11
12
  import { createPlugin } from './pm-plugins/main';
12
13
  import { pluginKey as mainPluginKey } from './pm-plugins/main/plugin-key';
13
14
  import { mergeUnconfirmedSteps } from './pm-plugins/mergeUnconfirmed';
15
+ import { monitorOrganic } from './pm-plugins/monitor-organic-changes';
14
16
  import { nativeCollabProviderPlugin } from './pm-plugins/native-collab-provider-plugin';
15
17
  import { sanitizeFilteredStep, createPlugin as trackSpammingStepsPlugin } from './pm-plugins/track-and-filter-spamming-steps';
16
18
  import { createPlugin as createLastOrganicChangePlugin, trackLastOrganicChangePluginKey } from './pm-plugins/track-last-organic-change';
@@ -146,7 +148,7 @@ export const collabEditPlugin = ({
146
148
  return content.every(node => {
147
149
  try {
148
150
  node.check(); // this will throw an error if the node is invalid
149
- } catch (error) {
151
+ } catch {
150
152
  return false;
151
153
  }
152
154
  return true;
@@ -249,6 +251,23 @@ export const collabEditPlugin = ({
249
251
  });
250
252
  }
251
253
  });
254
+ if (fg('platform_editor_collab_organic_change_reporting')) {
255
+ monitorOrganic({
256
+ api,
257
+ ...props,
258
+ onDataProcessed: data => {
259
+ var _api$analytics4, _api$analytics4$actio;
260
+ api === null || api === void 0 ? void 0 : (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 ? void 0 : (_api$analytics4$actio = _api$analytics4.actions) === null || _api$analytics4$actio === void 0 ? void 0 : _api$analytics4$actio.fireAnalyticsEvent({
261
+ action: ACTION.ORGANIC_CHANGES_TRACKED,
262
+ actionSubject: ACTION_SUBJECT.COLLAB,
263
+ attributes: {
264
+ organicChanges: data
265
+ },
266
+ eventType: EVENT_TYPE.OPERATIONAL
267
+ });
268
+ }
269
+ });
270
+ }
252
271
  },
253
272
  commands: {
254
273
  nudgeTelepointer: sessionId => ({
@@ -0,0 +1,120 @@
1
+ import { AnalyticsStep } from '@atlaskit/adf-schema/steps';
2
+ import { trackLastOrganicChangePluginKey } from './track-last-organic-change';
3
+ import { groupSteps, sanitizeStep } from './track-steps';
4
+ // This is essentially a queue of cached events. When the background task runs to send these items then this queue is flushed.
5
+ const organicReportingCache = [];
6
+ // Every ten seconds we will try to process the step data.
7
+ const LOW_PRIORITY_DELAY = 10000;
8
+
9
+ // See https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/
10
+
11
+ export const getScheduler = obj => {
12
+ if (!obj) {
13
+ return null;
14
+ }
15
+ if ('scheduler' in obj) {
16
+ return obj.scheduler;
17
+ }
18
+ return null;
19
+ };
20
+
21
+ /**
22
+ * Processes the steps metadata from the cache and calls the callback function with the processed data.
23
+ *
24
+ * @param {OrganicCacheType} cache - A cache containing steps metadata.
25
+ * @param {(data: OrganicMetadataAnalytics[]) => void} onDataProcessed - Callback function to be called with the processed data.
26
+ */
27
+ const task = (cache, onDataProcessed) => {
28
+ const data = [];
29
+ for (const item of cache) {
30
+ const {
31
+ steps,
32
+ ...rest
33
+ } = item;
34
+ // We'll use the same grouping and sanitize logic from the track-steps util
35
+ const stepTypesAmount = groupSteps(steps.map(sanitizeStep));
36
+ data.push({
37
+ ...rest,
38
+ stepTypesAmount
39
+ });
40
+ }
41
+
42
+ // clear the cache.
43
+ cache.length = 0;
44
+ if (data.length > 0) {
45
+ onDataProcessed(data);
46
+ }
47
+ };
48
+
49
+ /**
50
+ * Tracks the steps sent by the client by storing them in a cache and scheduling a task to process them. Once the steps are processed, the onDataProcessed callabck will be called.
51
+ *
52
+ * This is a non-critical code. If the browser doesn't support the Scheduler API https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/
53
+ *
54
+ * @param {TrackProps} props - The properties required for tracking steps.
55
+ * @param {ExtractInjectionAPI<CollabEditPlugin> | undefined} props.api - The API for the CollabEdit plugin.
56
+ * @param {EditorState} props.newEditorState - The new editor state.
57
+ * @param {Readonly<Transaction[]>} props.transactions - The transactions that contain the steps.
58
+ * @param {(data: OrganicMetadataAnalytics[]) => void} props.onDataProcessed - Callback function to be called with the processed data.
59
+ */
60
+ export const monitorOrganic = ({
61
+ newEditorState,
62
+ oldEditorState,
63
+ transactions,
64
+ onDataProcessed
65
+ }) => {
66
+ // We can exclude analytic steps since they should never trigger an organic change.
67
+ const newSteps = transactions.flatMap(t => t.steps).filter(step => !(step instanceof AnalyticsStep));
68
+ const scheduler = getScheduler(window);
69
+ if (!newSteps.length || !scheduler) {
70
+ return;
71
+ }
72
+
73
+ // We know that an organic change during startup will trigger a draft sync which will;
74
+ // fire editor edited event -> confluence/next/packages/editor-features/src/hooks/useDraftSync/useDraftSync.tsx
75
+ // and call triggerUpdate() notifying the BE that user edited the page -> confluence/next/packages/editor-features/src/hooks/useDraftSync/useEditorDraftSyncAction.tsx
76
+ // This can cause a problem with statsig metrics if organic changes are being incorrectly reported, ie an automated change
77
+ // occurs which contributes the user towards editing a page when in fact they didn't edit the page.
78
+ //
79
+ const oldPluginState = trackLastOrganicChangePluginKey.getState(oldEditorState);
80
+ const newPluginState = trackLastOrganicChangePluginKey.getState(newEditorState);
81
+ if (newSteps.length && (oldPluginState === null || oldPluginState === void 0 ? void 0 : oldPluginState.lastLocalOrganicBodyChangeAt) !== (newPluginState === null || newPluginState === void 0 ? void 0 : newPluginState.lastLocalOrganicBodyChangeAt)) {
82
+ const now = Date.now();
83
+ const isFirstChange = !(oldPluginState !== null && oldPluginState !== void 0 && oldPluginState.lastLocalOrganicBodyChangeAt);
84
+
85
+ // Check if we should compact with the previous entry (within 2 seconds) this means with a possible 10sec delay
86
+ // due to the postTask we could have a potential 5 grouped organic changes listed.
87
+ const shouldCompact = organicReportingCache.length > 0 && now - organicReportingCache[organicReportingCache.length - 1].startedAt < 2000;
88
+ if (shouldCompact) {
89
+ // Compact with previous entry
90
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
91
+ const prev = organicReportingCache.pop(); // We know it exists due to shouldCompact check
92
+
93
+ // tr.docChanged
94
+ organicReportingCache.push({
95
+ transactions: prev.transactions + 1,
96
+ startedAt: prev.startedAt,
97
+ endedAt: now,
98
+ isFirstChange: prev.isFirstChange || isFirstChange,
99
+ steps: prev.steps.concat(newSteps)
100
+ });
101
+ } else {
102
+ // Add new entry
103
+ organicReportingCache.push({
104
+ transactions: 1,
105
+ startedAt: now,
106
+ endedAt: now,
107
+ isFirstChange,
108
+ steps: newSteps
109
+ });
110
+ }
111
+ if (organicReportingCache.length === 1) {
112
+ scheduler.postTask(() => {
113
+ task(organicReportingCache, onDataProcessed);
114
+ }, {
115
+ priority: 'background',
116
+ delay: LOW_PRIORITY_DELAY
117
+ });
118
+ }
119
+ }
120
+ };
@@ -8,6 +8,7 @@ import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/anal
8
8
  import { isEmptyDocument } from '@atlaskit/editor-common/utils';
9
9
  import { JSONTransformer } from '@atlaskit/editor-json-transformer';
10
10
  import { AddMarkStep, AddNodeMarkStep } from '@atlaskit/editor-prosemirror/transform';
11
+ import { fg } from '@atlaskit/platform-feature-flags';
11
12
  import { collab, getCollabState, sendableSteps } from '@atlaskit/prosemirror-collab';
12
13
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
13
14
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
@@ -17,6 +18,7 @@ import { filterAnalyticsSteps } from './pm-plugins/filterAnalytics';
17
18
  import { createPlugin } from './pm-plugins/main';
18
19
  import { pluginKey as mainPluginKey } from './pm-plugins/main/plugin-key';
19
20
  import { mergeUnconfirmedSteps } from './pm-plugins/mergeUnconfirmed';
21
+ import { monitorOrganic } from './pm-plugins/monitor-organic-changes';
20
22
  import { nativeCollabProviderPlugin } from './pm-plugins/native-collab-provider-plugin';
21
23
  import { sanitizeFilteredStep, createPlugin as trackSpammingStepsPlugin } from './pm-plugins/track-and-filter-spamming-steps';
22
24
  import { createPlugin as createLastOrganicChangePlugin, trackLastOrganicChangePluginKey } from './pm-plugins/track-last-organic-change';
@@ -183,7 +185,7 @@ export var collabEditPlugin = function collabEditPlugin(_ref4) {
183
185
  return content.every(function (node) {
184
186
  try {
185
187
  node.check(); // this will throw an error if the node is invalid
186
- } catch (error) {
188
+ } catch (_unused) {
187
189
  return false;
188
190
  }
189
191
  return true;
@@ -294,6 +296,23 @@ export var collabEditPlugin = function collabEditPlugin(_ref4) {
294
296
  });
295
297
  }
296
298
  }));
299
+ if (fg('platform_editor_collab_organic_change_reporting')) {
300
+ monitorOrganic(_objectSpread(_objectSpread({
301
+ api: api
302
+ }, props), {}, {
303
+ onDataProcessed: function onDataProcessed(data) {
304
+ var _api$analytics4;
305
+ api === null || api === void 0 || (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 || (_api$analytics4 = _api$analytics4.actions) === null || _api$analytics4 === void 0 || _api$analytics4.fireAnalyticsEvent({
306
+ action: ACTION.ORGANIC_CHANGES_TRACKED,
307
+ actionSubject: ACTION_SUBJECT.COLLAB,
308
+ attributes: {
309
+ organicChanges: data
310
+ },
311
+ eventType: EVENT_TYPE.OPERATIONAL
312
+ });
313
+ }
314
+ }));
315
+ }
297
316
  },
298
317
  commands: {
299
318
  nudgeTelepointer: function nudgeTelepointer(sessionId) {
@@ -0,0 +1,137 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
3
+ var _excluded = ["steps"];
4
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
5
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
6
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
7
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
8
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
9
+ import { AnalyticsStep } from '@atlaskit/adf-schema/steps';
10
+ import { trackLastOrganicChangePluginKey } from './track-last-organic-change';
11
+ import { groupSteps, sanitizeStep } from './track-steps';
12
+ // This is essentially a queue of cached events. When the background task runs to send these items then this queue is flushed.
13
+ var organicReportingCache = [];
14
+ // Every ten seconds we will try to process the step data.
15
+ var LOW_PRIORITY_DELAY = 10000;
16
+
17
+ // See https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/
18
+
19
+ export var getScheduler = function getScheduler(obj) {
20
+ if (!obj) {
21
+ return null;
22
+ }
23
+ if ('scheduler' in obj) {
24
+ return obj.scheduler;
25
+ }
26
+ return null;
27
+ };
28
+
29
+ /**
30
+ * Processes the steps metadata from the cache and calls the callback function with the processed data.
31
+ *
32
+ * @param {OrganicCacheType} cache - A cache containing steps metadata.
33
+ * @param {(data: OrganicMetadataAnalytics[]) => void} onDataProcessed - Callback function to be called with the processed data.
34
+ */
35
+ var task = function task(cache, onDataProcessed) {
36
+ var data = [];
37
+ var _iterator = _createForOfIteratorHelper(cache),
38
+ _step;
39
+ try {
40
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
41
+ var item = _step.value;
42
+ var steps = item.steps,
43
+ rest = _objectWithoutProperties(item, _excluded);
44
+ // We'll use the same grouping and sanitize logic from the track-steps util
45
+ var stepTypesAmount = groupSteps(steps.map(sanitizeStep));
46
+ data.push(_objectSpread(_objectSpread({}, rest), {}, {
47
+ stepTypesAmount: stepTypesAmount
48
+ }));
49
+ }
50
+
51
+ // clear the cache.
52
+ } catch (err) {
53
+ _iterator.e(err);
54
+ } finally {
55
+ _iterator.f();
56
+ }
57
+ cache.length = 0;
58
+ if (data.length > 0) {
59
+ onDataProcessed(data);
60
+ }
61
+ };
62
+
63
+ /**
64
+ * Tracks the steps sent by the client by storing them in a cache and scheduling a task to process them. Once the steps are processed, the onDataProcessed callabck will be called.
65
+ *
66
+ * This is a non-critical code. If the browser doesn't support the Scheduler API https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/
67
+ *
68
+ * @param {TrackProps} props - The properties required for tracking steps.
69
+ * @param {ExtractInjectionAPI<CollabEditPlugin> | undefined} props.api - The API for the CollabEdit plugin.
70
+ * @param {EditorState} props.newEditorState - The new editor state.
71
+ * @param {Readonly<Transaction[]>} props.transactions - The transactions that contain the steps.
72
+ * @param {(data: OrganicMetadataAnalytics[]) => void} props.onDataProcessed - Callback function to be called with the processed data.
73
+ */
74
+ export var monitorOrganic = function monitorOrganic(_ref) {
75
+ var newEditorState = _ref.newEditorState,
76
+ oldEditorState = _ref.oldEditorState,
77
+ transactions = _ref.transactions,
78
+ onDataProcessed = _ref.onDataProcessed;
79
+ // We can exclude analytic steps since they should never trigger an organic change.
80
+ var newSteps = transactions.flatMap(function (t) {
81
+ return t.steps;
82
+ }).filter(function (step) {
83
+ return !(step instanceof AnalyticsStep);
84
+ });
85
+ var scheduler = getScheduler(window);
86
+ if (!newSteps.length || !scheduler) {
87
+ return;
88
+ }
89
+
90
+ // We know that an organic change during startup will trigger a draft sync which will;
91
+ // fire editor edited event -> confluence/next/packages/editor-features/src/hooks/useDraftSync/useDraftSync.tsx
92
+ // and call triggerUpdate() notifying the BE that user edited the page -> confluence/next/packages/editor-features/src/hooks/useDraftSync/useEditorDraftSyncAction.tsx
93
+ // This can cause a problem with statsig metrics if organic changes are being incorrectly reported, ie an automated change
94
+ // occurs which contributes the user towards editing a page when in fact they didn't edit the page.
95
+ //
96
+ var oldPluginState = trackLastOrganicChangePluginKey.getState(oldEditorState);
97
+ var newPluginState = trackLastOrganicChangePluginKey.getState(newEditorState);
98
+ if (newSteps.length && (oldPluginState === null || oldPluginState === void 0 ? void 0 : oldPluginState.lastLocalOrganicBodyChangeAt) !== (newPluginState === null || newPluginState === void 0 ? void 0 : newPluginState.lastLocalOrganicBodyChangeAt)) {
99
+ var now = Date.now();
100
+ var isFirstChange = !(oldPluginState !== null && oldPluginState !== void 0 && oldPluginState.lastLocalOrganicBodyChangeAt);
101
+
102
+ // Check if we should compact with the previous entry (within 2 seconds) this means with a possible 10sec delay
103
+ // due to the postTask we could have a potential 5 grouped organic changes listed.
104
+ var shouldCompact = organicReportingCache.length > 0 && now - organicReportingCache[organicReportingCache.length - 1].startedAt < 2000;
105
+ if (shouldCompact) {
106
+ // Compact with previous entry
107
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
108
+ var prev = organicReportingCache.pop(); // We know it exists due to shouldCompact check
109
+
110
+ // tr.docChanged
111
+ organicReportingCache.push({
112
+ transactions: prev.transactions + 1,
113
+ startedAt: prev.startedAt,
114
+ endedAt: now,
115
+ isFirstChange: prev.isFirstChange || isFirstChange,
116
+ steps: prev.steps.concat(newSteps)
117
+ });
118
+ } else {
119
+ // Add new entry
120
+ organicReportingCache.push({
121
+ transactions: 1,
122
+ startedAt: now,
123
+ endedAt: now,
124
+ isFirstChange: isFirstChange,
125
+ steps: newSteps
126
+ });
127
+ }
128
+ if (organicReportingCache.length === 1) {
129
+ scheduler.postTask(function () {
130
+ task(organicReportingCache, onDataProcessed);
131
+ }, {
132
+ priority: 'background',
133
+ delay: LOW_PRIORITY_DELAY
134
+ });
135
+ }
136
+ }
137
+ };
@@ -0,0 +1,39 @@
1
+ import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
2
+ import type { EditorState, Transaction } from '@atlaskit/editor-prosemirror/state';
3
+ import type { CollabEditPlugin } from '../collabEditPluginType';
4
+ export type OrganicMetadataAnalytics = {
5
+ endedAt: number;
6
+ isFirstChange: boolean;
7
+ startedAt: number;
8
+ stepTypesAmount: {
9
+ [key: string]: number;
10
+ };
11
+ transactions: number;
12
+ };
13
+ type TrackProps = {
14
+ api: ExtractInjectionAPI<CollabEditPlugin> | undefined;
15
+ newEditorState: EditorState;
16
+ oldEditorState: EditorState;
17
+ onDataProcessed: (data: OrganicMetadataAnalytics[]) => void;
18
+ transactions: Readonly<Transaction[]>;
19
+ };
20
+ type Scheduler = {
21
+ postTask: (cb: () => void, options: {
22
+ delay: number;
23
+ priority: 'background';
24
+ }) => Promise<unknown>;
25
+ };
26
+ export declare const getScheduler: (obj: any) => Scheduler | null;
27
+ /**
28
+ * Tracks the steps sent by the client by storing them in a cache and scheduling a task to process them. Once the steps are processed, the onDataProcessed callabck will be called.
29
+ *
30
+ * This is a non-critical code. If the browser doesn't support the Scheduler API https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/
31
+ *
32
+ * @param {TrackProps} props - The properties required for tracking steps.
33
+ * @param {ExtractInjectionAPI<CollabEditPlugin> | undefined} props.api - The API for the CollabEdit plugin.
34
+ * @param {EditorState} props.newEditorState - The new editor state.
35
+ * @param {Readonly<Transaction[]>} props.transactions - The transactions that contain the steps.
36
+ * @param {(data: OrganicMetadataAnalytics[]) => void} props.onDataProcessed - Callback function to be called with the processed data.
37
+ */
38
+ export declare const monitorOrganic: ({ newEditorState, oldEditorState, transactions, onDataProcessed, }: TrackProps) => void;
39
+ export {};
@@ -0,0 +1,39 @@
1
+ import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
2
+ import type { EditorState, Transaction } from '@atlaskit/editor-prosemirror/state';
3
+ import type { CollabEditPlugin } from '../collabEditPluginType';
4
+ export type OrganicMetadataAnalytics = {
5
+ endedAt: number;
6
+ isFirstChange: boolean;
7
+ startedAt: number;
8
+ stepTypesAmount: {
9
+ [key: string]: number;
10
+ };
11
+ transactions: number;
12
+ };
13
+ type TrackProps = {
14
+ api: ExtractInjectionAPI<CollabEditPlugin> | undefined;
15
+ newEditorState: EditorState;
16
+ oldEditorState: EditorState;
17
+ onDataProcessed: (data: OrganicMetadataAnalytics[]) => void;
18
+ transactions: Readonly<Transaction[]>;
19
+ };
20
+ type Scheduler = {
21
+ postTask: (cb: () => void, options: {
22
+ delay: number;
23
+ priority: 'background';
24
+ }) => Promise<unknown>;
25
+ };
26
+ export declare const getScheduler: (obj: any) => Scheduler | null;
27
+ /**
28
+ * Tracks the steps sent by the client by storing them in a cache and scheduling a task to process them. Once the steps are processed, the onDataProcessed callabck will be called.
29
+ *
30
+ * This is a non-critical code. If the browser doesn't support the Scheduler API https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/
31
+ *
32
+ * @param {TrackProps} props - The properties required for tracking steps.
33
+ * @param {ExtractInjectionAPI<CollabEditPlugin> | undefined} props.api - The API for the CollabEdit plugin.
34
+ * @param {EditorState} props.newEditorState - The new editor state.
35
+ * @param {Readonly<Transaction[]>} props.transactions - The transactions that contain the steps.
36
+ * @param {(data: OrganicMetadataAnalytics[]) => void} props.onDataProcessed - Callback function to be called with the processed data.
37
+ */
38
+ export declare const monitorOrganic: ({ newEditorState, oldEditorState, transactions, onDataProcessed, }: TrackProps) => void;
39
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-collab-edit",
3
- "version": "7.2.0",
3
+ "version": "7.2.1",
4
4
  "description": "Collab Edit plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -39,12 +39,12 @@
39
39
  "@atlaskit/frontend-utilities": "^3.2.0",
40
40
  "@atlaskit/platform-feature-flags": "^1.1.0",
41
41
  "@atlaskit/prosemirror-collab": "^0.22.0",
42
- "@atlaskit/tmp-editor-statsig": "^15.11.0",
42
+ "@atlaskit/tmp-editor-statsig": "^15.15.0",
43
43
  "@babel/runtime": "^7.0.0",
44
44
  "memoize-one": "^6.0.0"
45
45
  },
46
46
  "peerDependencies": {
47
- "@atlaskit/editor-common": "^110.44.0",
47
+ "@atlaskit/editor-common": "^110.46.0",
48
48
  "react": "^18.2.0",
49
49
  "react-dom": "^18.2.0"
50
50
  },
@@ -94,6 +94,9 @@
94
94
  },
95
95
  "platform_editor_inorganic_batchattrsstep_localid": {
96
96
  "type": "boolean"
97
+ },
98
+ "platform_editor_collab_organic_change_reporting": {
99
+ "type": "boolean"
97
100
  }
98
101
  }
99
102
  }