@atlaskit/editor-plugin-interactivity 0.2.0 → 1.0.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +31 -0
  3. package/dist/cjs/collector/editor-event-observer.js +63 -0
  4. package/dist/cjs/collector/interaction-events.js +62 -0
  5. package/dist/cjs/collector/interaction-group.js +20 -2
  6. package/dist/cjs/collector/interaction-tracker.js +62 -21
  7. package/dist/cjs/collector/interactivity-collector.js +54 -2
  8. package/dist/cjs/collector/interactivity-session.js +3 -0
  9. package/dist/cjs/interactivityPlugin.js +16 -4
  10. package/dist/es2019/collector/editor-event-observer.js +46 -0
  11. package/dist/es2019/collector/interaction-events.js +55 -0
  12. package/dist/es2019/collector/interaction-group.js +17 -2
  13. package/dist/es2019/collector/interaction-tracker.js +60 -20
  14. package/dist/es2019/collector/interactivity-collector.js +48 -2
  15. package/dist/es2019/collector/interactivity-session.js +3 -0
  16. package/dist/es2019/interactivityPlugin.js +17 -5
  17. package/dist/esm/collector/editor-event-observer.js +57 -0
  18. package/dist/esm/collector/interaction-events.js +55 -0
  19. package/dist/esm/collector/interaction-group.js +20 -2
  20. package/dist/esm/collector/interaction-tracker.js +63 -21
  21. package/dist/esm/collector/interactivity-collector.js +54 -2
  22. package/dist/esm/collector/interactivity-session.js +3 -0
  23. package/dist/esm/interactivityPlugin.js +17 -5
  24. package/dist/types/analytics/interactivity-snapshot.d.ts +5 -0
  25. package/dist/types/collector/editor-event-observer.d.ts +19 -0
  26. package/dist/types/collector/interaction-events.d.ts +19 -0
  27. package/dist/types/collector/interaction-group.d.ts +14 -2
  28. package/dist/types/collector/interaction-tracker.d.ts +12 -3
  29. package/dist/types/collector/interactivity-collector.d.ts +15 -0
  30. package/dist/types/collector/interactivity-session.d.ts +3 -0
  31. package/docs/0-intro.tsx +2 -1
  32. package/package.json +5 -5
@@ -4,6 +4,8 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
4
  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; } } }; }
5
5
  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; } }
6
6
  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; }
7
+ import { interactionEventKind } from './interaction-events';
8
+
7
9
  /**
8
10
  * The Event Timing fields this package reads. `interactionId` is missing from the DOM
9
11
  * typings' `PerformanceEventTiming`, so it is declared here as optional, which also makes
@@ -13,12 +15,13 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
13
15
  /**
14
16
  * What an entry did to the interaction it belongs to: either it is the first entry of a new
15
17
  * interaction, or it measured an interaction that was already counted as slower than it was
16
- * known to be.
18
+ * known to be. Both carry the editor group of the interaction, if it is one of the editor's.
17
19
  */
18
20
 
19
21
  /**
20
- * How many interactions are remembered, so their growth can still be applied. Entries of one
21
- * interaction arrive within the interaction itself, so anything older is not needed.
22
+ * How many interactions are remembered, so their growth can still be applied, and how many of
23
+ * the editor's events. Entries of one interaction arrive within the interaction itself, so
24
+ * anything older than the last few hundred is not needed.
22
25
  */
23
26
  var MAX_TRACKED = 256;
24
27
  /**
@@ -28,7 +31,17 @@ var MAX_TRACKED = 256;
28
31
  var PRUNE_BATCH = 64;
29
32
 
30
33
  /**
31
- * Groups Event Timing entries into interactions.
34
+ * Identifies the event an entry measured: an entry's `startTime` is that event's timestamp and its
35
+ * `name` is its type. The type is in the key because browsers coarsen the timestamp, so two events
36
+ * of one task can share it — and events of one type are always of one group, so a collision cannot
37
+ * move an interaction into another group.
38
+ */
39
+ function eventKey(type, timeStamp) {
40
+ return "".concat(type, "|").concat(timeStamp);
41
+ }
42
+
43
+ /**
44
+ * Makes interactions out of what the two observers report, for one session.
32
45
  *
33
46
  * Entries sharing a non-zero `interactionId` are one interaction whose latency is the
34
47
  * maximum `duration` among them. Entries arrive incrementally, so an interaction's latency
@@ -38,6 +51,9 @@ var PRUNE_BATCH = 64;
38
51
  * A non-zero `interactionId` is the browser's own definition of an interaction, which is
39
52
  * also what INP filters on: it is assigned to the pointer and keyboard events that make one
40
53
  * up, and never to scrolling or pointer movement.
54
+ *
55
+ * The editor's events answer what an entry cannot: which interactions were with the editor, and
56
+ * how many there were, including the ones below the Event Timing reporting threshold.
41
57
  */
42
58
  export var InteractionTracker = /*#__PURE__*/function () {
43
59
  /**
@@ -50,7 +66,8 @@ export var InteractionTracker = /*#__PURE__*/function () {
50
66
  function InteractionTracker() {
51
67
  var startsAfterInteractionId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
52
68
  _classCallCheck(this, InteractionTracker);
53
- _defineProperty(this, "latencies", new Map());
69
+ _defineProperty(this, "interactions", new Map());
70
+ _defineProperty(this, "groupByEvent", new Map());
54
71
  _defineProperty(this, "highestInteractionId", 0);
55
72
  this.startsAfterInteractionId = startsAfterInteractionId;
56
73
  }
@@ -87,41 +104,66 @@ export var InteractionTracker = /*#__PURE__*/function () {
87
104
  if (!Number.isFinite(entry.duration) || entry.duration < 0) {
88
105
  return undefined;
89
106
  }
90
- var previousLatencyMs = this.latencies.get(interactionId);
91
- if (previousLatencyMs === undefined) {
92
- this.latencies.set(interactionId, entry.duration);
93
- this.prune();
107
+ var tracked = this.interactions.get(interactionId);
108
+ if (tracked === undefined) {
109
+ // Taken once: an entry that only makes the interaction slower has to move its count
110
+ // within the group it was counted in, not into another one.
111
+ var group = this.groupByEvent.get(eventKey(entry.name, entry.startTime));
112
+ this.interactions.set(interactionId, {
113
+ latencyMs: entry.duration,
114
+ group: group
115
+ });
116
+ this.prune(this.interactions);
94
117
  return {
95
118
  type: 'new',
96
- latencyMs: entry.duration
119
+ latencyMs: entry.duration,
120
+ group: group
97
121
  };
98
122
  }
99
- if (entry.duration <= previousLatencyMs) {
123
+ if (entry.duration <= tracked.latencyMs) {
100
124
  return undefined;
101
125
  }
102
- this.latencies.set(interactionId, entry.duration);
126
+ var fromMs = tracked.latencyMs;
127
+ tracked.latencyMs = entry.duration;
103
128
  return {
104
129
  type: 'remeasured',
105
- fromMs: previousLatencyMs,
106
- toMs: entry.duration
130
+ fromMs: fromMs,
131
+ toMs: entry.duration,
132
+ group: tracked.group
107
133
  };
108
134
  }
135
+
136
+ /** @returns the group of an interaction to count, when this is the event its group counts on. */
137
+ }, {
138
+ key: "recordEditorEvent",
139
+ value: function recordEditorEvent(event) {
140
+ var kind = interactionEventKind(event.type);
141
+ if (!kind) {
142
+ return undefined;
143
+ }
144
+
145
+ // Every event of the interaction, because any of them can be the one Event Timing reports
146
+ // as the slowest: for a pointer press that is usually the click.
147
+ this.groupByEvent.set(eventKey(event.type, event.timeStamp), kind.group);
148
+ this.prune(this.groupByEvent);
149
+ return kind.counts ? kind.group : undefined;
150
+ }
109
151
  }, {
110
152
  key: "prune",
111
- value: function prune() {
112
- if (this.latencies.size <= MAX_TRACKED) {
153
+ value: function prune(entries) {
154
+ if (entries.size <= MAX_TRACKED) {
113
155
  return;
114
156
  }
115
157
 
116
- // `Map` preserves insertion order and `interactionId` increases monotonically,
117
- // so the entries inserted first are the least likely to see another entry.
158
+ // `Map` preserves insertion order, so the entries inserted first are the least likely to
159
+ // see another entry or another event.
118
160
  var remaining = PRUNE_BATCH;
119
- var _iterator = _createForOfIteratorHelper(this.latencies.keys()),
161
+ var _iterator = _createForOfIteratorHelper(entries.keys()),
120
162
  _step;
121
163
  try {
122
164
  for (_iterator.s(); !(_step = _iterator.n()).done;) {
123
- var interactionId = _step.value;
124
- this.latencies.delete(interactionId);
165
+ var key = _step.value;
166
+ entries.delete(key);
125
167
  remaining -= 1;
126
168
  if (remaining === 0) {
127
169
  return;
@@ -5,6 +5,7 @@ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol
5
5
  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; } }
6
6
  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; }
7
7
  import { getDocument } from '@atlaskit/browser-apis';
8
+ import { EditorEventObserver } from './editor-event-observer';
8
9
  import { InteractionObserver } from './interaction-observer';
9
10
  import { InteractivitySession } from './interactivity-session';
10
11
  import { SCHEMA_VERSION } from './bucket-boundaries';
@@ -13,6 +14,9 @@ import { SnapshotScheduler } from './snapshot-scheduler';
13
14
  /**
14
15
  * Collects interaction latencies for one editor mount and emits session-to-date snapshots.
15
16
  *
17
+ * Every interaction is counted in `page`, and the ones with the editor again in one of the editor
18
+ * groups, which is what tells a slow editor apart from a slow page around it.
19
+ *
16
20
  * `start` and `stop` bound the collecting, which happens once. Within it there can be several
17
21
  * sessions, because a session covers one document in one mode: a Confluence live page
18
22
  * navigates and switches between reading and editing without ever remounting the editor, and
@@ -32,6 +36,9 @@ export var InteractivityCollector = /*#__PURE__*/function () {
32
36
  this.interactionObserver = new InteractionObserver(function (entries) {
33
37
  return _this.recordEntries(entries);
34
38
  });
39
+ this.editorEvents = new EditorEventObserver(function (event) {
40
+ return _this.recordEditorEvent(event);
41
+ });
35
42
  this.snapshotScheduler = new SnapshotScheduler(function () {
36
43
  return _this.onTimer();
37
44
  });
@@ -76,6 +83,7 @@ export var InteractivityCollector = /*#__PURE__*/function () {
76
83
  // this object was constructed.
77
84
  this.session = this.createSession(0);
78
85
  this.interactionObserver.start();
86
+ this.editorEvents.observe(this.editorRoot);
79
87
  this.snapshotScheduler.start();
80
88
  this.lifecycleObserver.start();
81
89
  this.started = true;
@@ -92,10 +100,24 @@ export var InteractivityCollector = /*#__PURE__*/function () {
92
100
  this.takeSnapshot('unmount');
93
101
  this.stopped = true;
94
102
  this.interactionObserver.stop();
103
+ this.editorEvents.stop();
95
104
  this.snapshotScheduler.stop();
96
105
  this.lifecycleObserver.stop();
97
106
  }
98
107
 
108
+ /**
109
+ * The element the editor renders itself into is what makes an interaction one of the editor's.
110
+ * The session is not tied to it: interactions from before it arrives are counted in `page`.
111
+ */
112
+ }, {
113
+ key: "setEditorRoot",
114
+ value: function setEditorRoot(root) {
115
+ this.editorRoot = root !== null && root !== void 0 ? root : undefined;
116
+ if (this.started && !this.stopped) {
117
+ this.editorEvents.observe(this.editorRoot);
118
+ }
119
+ }
120
+
99
121
  /**
100
122
  * Rotates the session when the editor is pointed at different content.
101
123
  *
@@ -205,6 +227,26 @@ export var InteractivityCollector = /*#__PURE__*/function () {
205
227
  value: function onPageShow() {
206
228
  this.session.lifecycleSnapshotEmitted = false;
207
229
  }
230
+
231
+ /**
232
+ * Counts an interaction with the editor, including the ones Event Timing never reports because
233
+ * they were faster than its threshold — which is why a count alone moves the session on.
234
+ */
235
+ }, {
236
+ key: "recordEditorEvent",
237
+ value: function recordEditorEvent(event) {
238
+ if (this.stopped) {
239
+ return;
240
+ }
241
+ var group = this.session.tracker.recordEditorEvent(event);
242
+ if (!group) {
243
+ return;
244
+ }
245
+
246
+ // The session names its editor groups after the fields they are reported in.
247
+ this.session[group].countTotal();
248
+ this.session.revision += 1;
249
+ }
208
250
  }, {
209
251
  key: "recordEntries",
210
252
  value: function recordEntries(entries) {
@@ -222,8 +264,14 @@ export var InteractivityCollector = /*#__PURE__*/function () {
222
264
  }
223
265
  if (update.type === 'new') {
224
266
  page.add(update.latencyMs);
267
+ if (update.group) {
268
+ this.session[update.group].add(update.latencyMs);
269
+ }
225
270
  } else {
226
271
  page.remeasure(update.fromMs, update.toMs);
272
+ if (update.group) {
273
+ this.session[update.group].remeasure(update.fromMs, update.toMs);
274
+ }
227
275
  }
228
276
  this.session.revision += 1;
229
277
  }
@@ -263,7 +311,7 @@ export var InteractivityCollector = /*#__PURE__*/function () {
263
311
  session.emittedRevision = session.revision;
264
312
  var now = performance.now();
265
313
  var hiddenMs = session.hiddenMs + (session.hiddenSince === undefined ? 0 : now - session.hiddenSince);
266
- var totalCount = InteractionObserver.readPageInteractionCount() - session.interactionCountAtStart;
314
+ var pageTotalCount = InteractionObserver.readPageInteractionCount() - session.interactionCountAtStart;
267
315
  this.emit({
268
316
  schema: SCHEMA_VERSION,
269
317
  interactivitySessionId: session.id,
@@ -275,7 +323,11 @@ export var InteractivityCollector = /*#__PURE__*/function () {
275
323
  hiddenMs: Math.round(hiddenMs),
276
324
  nodeSize: this.getNodeSize(),
277
325
  editorDomSize: this.getEditorDomSize(),
278
- page: session.page.snapshot(totalCount)
326
+ // Only `page` is told its total; each editor group has counted its own.
327
+ page: session.page.snapshot(pageTotalCount),
328
+ editorTyping: session.editorTyping.snapshot(),
329
+ editorPointer: session.editorPointer.snapshot(),
330
+ editorOther: session.editorOther.snapshot()
279
331
  });
280
332
  }
281
333
  }]);
@@ -27,6 +27,9 @@ export var InteractivitySession = /*#__PURE__*/_createClass(function Interactivi
27
27
  /** Page interaction count when the session opened, subtracted to get its own total. */
28
28
  _defineProperty(this, "interactionCountAtStart", InteractionObserver.readPageInteractionCount());
29
29
  _defineProperty(this, "page", new InteractionGroup());
30
+ _defineProperty(this, "editorTyping", new InteractionGroup());
31
+ _defineProperty(this, "editorPointer", new InteractionGroup());
32
+ _defineProperty(this, "editorOther", new InteractionGroup());
30
33
  /** Increments per snapshot; a query takes the highest one per session. */
31
34
  _defineProperty(this, "seq", 0);
32
35
  _defineProperty(this, "hiddenMs", 0);
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useRef } from 'react';
2
2
  import { logException } from '@atlaskit/editor-common/monitoring';
3
3
  import { fireInteractivityEvent } from './analytics/fire-interactivity-event';
4
4
  import { InteractivityCollector } from './collector/interactivity-collector';
@@ -16,11 +16,10 @@ export var interactivityPlugin = function interactivityPlugin(_ref) {
16
16
  return {
17
17
  name: 'interactivity',
18
18
  usePluginHook: function usePluginHook(_ref2) {
19
- var editorView = _ref2.editorView;
19
+ var editorView = _ref2.editorView,
20
+ wrapperElement = _ref2.wrapperElement;
21
+ var collectorRef = useRef(undefined);
20
22
  useEffect(function () {
21
- // Nothing measured here is worth an editor. This effect runs inside the plugin slot's
22
- // error boundary, which also renders every plugin's content components, so a throw
23
- // from instrumentation would take that UI down with it.
24
23
  try {
25
24
  var _api$contextIdentifie2, _api$editorViewMode2;
26
25
  var collector = new InteractivityCollector({
@@ -51,6 +50,9 @@ export var interactivityPlugin = function interactivityPlugin(_ref) {
51
50
  return editorView.isDestroyed ? undefined : editorView.dom.getElementsByTagName('*').length;
52
51
  }
53
52
  });
53
+
54
+ // Effects run in declaration order, so the one below always finds it.
55
+ collectorRef.current = collector;
54
56
  if (!collector.start()) {
55
57
  return;
56
58
  }
@@ -82,6 +84,16 @@ export var interactivityPlugin = function interactivityPlugin(_ref) {
82
84
  // live registry, and restarting the session on a reconfigure would split one
83
85
  // editor session in two.
84
86
  }, [editorView]);
87
+ useEffect(function () {
88
+ try {
89
+ var _collectorRef$current;
90
+ (_collectorRef$current = collectorRef.current) === null || _collectorRef$current === void 0 || _collectorRef$current.setEditorRoot(wrapperElement);
91
+ } catch (error) {
92
+ void logException(error, {
93
+ location: 'editor-plugin-interactivity/observe'
94
+ });
95
+ }
96
+ }, [wrapperElement]);
85
97
  }
86
98
  };
87
99
  };
@@ -48,10 +48,15 @@ export type InteractionGroupSnapshot = {
48
48
  export type InteractivitySnapshot = {
49
49
  activeMs: number;
50
50
  editorDomSize?: number;
51
+ /** Interactions inside the editor that are neither typing nor pointing. */
52
+ editorOther: InteractionGroupSnapshot;
53
+ editorPointer: InteractionGroupSnapshot;
54
+ editorTyping: InteractionGroupSnapshot;
51
55
  hiddenMs: number;
52
56
  interactivitySessionId: string;
53
57
  nodeSize?: number;
54
58
  objectId?: string;
59
+ /** Every interaction on the page, the editor groups included. */
55
60
  page: InteractionGroupSnapshot;
56
61
  reason: SnapshotReason;
57
62
  schema: number;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Reports the events the interactions with the editor are made of, as the browser dispatched
3
+ * them. What they mean is the tracker's business.
4
+ *
5
+ * Event Timing cannot answer which interactions were with the editor, because it observes the
6
+ * whole document, nor how many there were, because it reports none below 16 ms.
7
+ */
8
+ export declare class EditorEventObserver {
9
+ private readonly onEvent;
10
+ private root;
11
+ private unbind;
12
+ constructor(onEvent: (event: Event) => void);
13
+ /**
14
+ * Called as the element the editor renders itself into changes: the editor only knows it after
15
+ * its first render, and can replace it.
16
+ */
17
+ observe(root: Element | null | undefined): void;
18
+ stop(): void;
19
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The editor group an interaction belongs to, which is also the field the event reports it in.
3
+ *
4
+ * `editorOther` is the remainder — an interaction the browser counts that is neither typing nor
5
+ * pointing — so the three groups together cover whatever the browser calls an interaction.
6
+ */
7
+ export type EditorInteractionGroupName = 'editorOther' | 'editorPointer' | 'editorTyping';
8
+ export type InteractionEventKind = {
9
+ /**
10
+ * Whether an interaction is counted when this event arrives. One event of each group has it,
11
+ * because an interaction is several events — `keydown`, what the key produced, `keyup`, or
12
+ * `pointerdown`, `pointerup`, `click` — and counting each would count it three times.
13
+ */
14
+ counts?: boolean;
15
+ group: EditorInteractionGroupName;
16
+ };
17
+ /** Derived, so the types listened for cannot drift from the table. */
18
+ export declare const INTERACTION_EVENT_TYPES: string[];
19
+ export declare function interactionEventKind(type: string): InteractionEventKind | undefined;
@@ -7,16 +7,28 @@ import type { InteractionGroupSnapshot } from '../analytics/interactivity-snapsh
7
7
  * whatever its latency was, and everything the event carries — the reported buckets, the
8
8
  * count, the sum, the maximum and the percentiles — is derived from that map when a snapshot
9
9
  * is taken. Nothing is computed while interactions arrive.
10
+ *
11
+ * The two counters count different populations: `add` takes the interactions Event Timing
12
+ * measured, `countTotal` takes all of them, including the ones below the 16 ms reporting threshold
13
+ * it never delivers. So `totalCount >= observedCount`, and the difference is how many were too
14
+ * fast to be measured.
10
15
  */
11
16
  export declare class InteractionGroup {
12
17
  private countByLatency;
18
+ private totalCount;
13
19
  add(latencyMs: number): void;
20
+ /**
21
+ * Counts an interaction towards the group's total, measured or not. `page` has no use for it:
22
+ * `performance.interactionCount` counts the page's interactions.
23
+ */
24
+ countTotal(): void;
14
25
  remeasure(previousLatencyMs: number, latencyMs: number): void;
15
26
  /**
16
27
  * @param totalCount every interaction of the group, including those below the Event Timing
17
- * reporting threshold, which this group never sees.
28
+ * reporting threshold. Defaults to what `countTotal` was told, which is where an editor
29
+ * group's total comes from; `page` passes `performance.interactionCount` instead.
18
30
  */
19
- snapshot(totalCount: number): InteractionGroupSnapshot;
31
+ snapshot(totalCount?: number): InteractionGroupSnapshot;
20
32
  private observedCount;
21
33
  /** Rounding lives here so that every count goes through the same step, in or out. */
22
34
  private roundLatencyUp;
@@ -1,3 +1,4 @@
1
+ import type { EditorInteractionGroupName } from './interaction-events';
1
2
  /**
2
3
  * The Event Timing fields this package reads. `interactionId` is missing from the DOM
3
4
  * typings' `PerformanceEventTiming`, so it is declared here as optional, which also makes
@@ -9,18 +10,20 @@ export type InteractionEntry = PerformanceEntry & {
9
10
  /**
10
11
  * What an entry did to the interaction it belongs to: either it is the first entry of a new
11
12
  * interaction, or it measured an interaction that was already counted as slower than it was
12
- * known to be.
13
+ * known to be. Both carry the editor group of the interaction, if it is one of the editor's.
13
14
  */
14
15
  export type InteractionUpdate = {
16
+ group: EditorInteractionGroupName | undefined;
15
17
  latencyMs: number;
16
18
  type: 'new';
17
19
  } | {
18
20
  fromMs: number;
21
+ group: EditorInteractionGroupName | undefined;
19
22
  toMs: number;
20
23
  type: 'remeasured';
21
24
  };
22
25
  /**
23
- * Groups Event Timing entries into interactions.
26
+ * Makes interactions out of what the two observers report, for one session.
24
27
  *
25
28
  * Entries sharing a non-zero `interactionId` are one interaction whose latency is the
26
29
  * maximum `duration` among them. Entries arrive incrementally, so an interaction's latency
@@ -30,10 +33,14 @@ export type InteractionUpdate = {
30
33
  * A non-zero `interactionId` is the browser's own definition of an interaction, which is
31
34
  * also what INP filters on: it is assigned to the pointer and keyboard events that make one
32
35
  * up, and never to scrolling or pointer movement.
36
+ *
37
+ * The editor's events answer what an entry cannot: which interactions were with the editor, and
38
+ * how many there were, including the ones below the Event Timing reporting threshold.
33
39
  */
34
40
  export declare class InteractionTracker {
35
- private latencies;
36
41
  private readonly startsAfterInteractionId;
42
+ private readonly interactions;
43
+ private readonly groupByEvent;
37
44
  private highestInteractionId;
38
45
  /**
39
46
  * @param startsAfterInteractionId interactions up to and including this one belong to the
@@ -52,5 +59,7 @@ export declare class InteractionTracker {
52
59
  * part of an interaction, belongs to a previous tracker, or does not change one.
53
60
  */
54
61
  merge(entry: InteractionEntry): InteractionUpdate | undefined;
62
+ /** @returns the group of an interaction to count, when this is the event its group counts on. */
63
+ recordEditorEvent(event: Event): EditorInteractionGroupName | undefined;
55
64
  private prune;
56
65
  }
@@ -11,6 +11,9 @@ export type InteractivityCollectorOptions = {
11
11
  /**
12
12
  * Collects interaction latencies for one editor mount and emits session-to-date snapshots.
13
13
  *
14
+ * Every interaction is counted in `page`, and the ones with the editor again in one of the editor
15
+ * groups, which is what tells a slow editor apart from a slow page around it.
16
+ *
14
17
  * `start` and `stop` bound the collecting, which happens once. Within it there can be several
15
18
  * sessions, because a session covers one document in one mode: a Confluence live page
16
19
  * navigates and switches between reading and editing without ever remounting the editor, and
@@ -23,9 +26,11 @@ export declare class InteractivityCollector {
23
26
  private readonly getObjectId;
24
27
  private readonly getSessionMode;
25
28
  private readonly interactionObserver;
29
+ private readonly editorEvents;
26
30
  private readonly snapshotScheduler;
27
31
  private readonly lifecycleObserver;
28
32
  private session;
33
+ private editorRoot;
29
34
  private started;
30
35
  private stopped;
31
36
  constructor(options: InteractivityCollectorOptions);
@@ -38,6 +43,11 @@ export declare class InteractivityCollector {
38
43
  start(): boolean;
39
44
  /** Reports the session as ended by the editor unmounting, and stops collecting. */
40
45
  stop(): void;
46
+ /**
47
+ * The element the editor renders itself into is what makes an interaction one of the editor's.
48
+ * The session is not tied to it: interactions from before it arrives are counted in `page`.
49
+ */
50
+ setEditorRoot(root: Element | null | undefined): void;
41
51
  /**
42
52
  * Rotates the session when the editor is pointed at different content.
43
53
  *
@@ -70,6 +80,11 @@ export declare class InteractivityCollector {
70
80
  * one is due.
71
81
  */
72
82
  private onPageShow;
83
+ /**
84
+ * Counts an interaction with the editor, including the ones Event Timing never reports because
85
+ * they were faster than its threshold — which is why a count alone moves the session on.
86
+ */
87
+ private recordEditorEvent;
73
88
  private recordEntries;
74
89
  private takeSnapshot;
75
90
  }
@@ -20,6 +20,9 @@ export declare class InteractivitySession {
20
20
  readonly interactionCountAtStart: number;
21
21
  readonly tracker: InteractionTracker;
22
22
  readonly page: InteractionGroup;
23
+ readonly editorTyping: InteractionGroup;
24
+ readonly editorPointer: InteractionGroup;
25
+ readonly editorOther: InteractionGroup;
23
26
  /** Increments per snapshot; a query takes the highest one per session. */
24
27
  seq: number;
25
28
  /** Both fixed for the session: a change to either closes it and opens the next. */
package/docs/0-intro.tsx CHANGED
@@ -24,7 +24,8 @@ ${createEditorUseOnlyNotice('Editor Plugin Interactivity', [
24
24
  This package includes the interactivity plugin used by \`@atlaskit/editor-core\`.
25
25
 
26
26
  It reports the \`editor interactivity\` operational event: session-to-date interaction latency
27
- histograms for full page editor sessions, per [RFC 095](https://hello.atlassian.net/wiki/spaces/EDITOR/pages/7527607488/Editor+RFC+095+Confluence+editor+responsiveness+bucketed+INP+telemetry).
27
+ histograms for full page editor sessions — for the page as a whole, and for the editor's typing,
28
+ pointer and other interactions — per [RFC 095](https://hello.atlassian.net/wiki/spaces/EDITOR/pages/7527607488/Editor+RFC+095+Confluence+editor+responsiveness+bucketed+INP+telemetry).
28
29
  See the package README for the event shape, the snapshot cadence and what ends a session.
29
30
 
30
31
  The plugin has no configuration, state, actions or commands — adding it to a preset is what turns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-interactivity",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "Interactivity plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -20,14 +20,14 @@
20
20
  "sideEffects": false,
21
21
  "dependencies": {
22
22
  "@atlaskit/browser-apis": "^1.2.0",
23
- "@atlaskit/editor-plugin-analytics": "^15.0.0",
24
- "@atlaskit/editor-plugin-context-identifier": "^15.0.0",
25
- "@atlaskit/editor-plugin-editor-viewmode": "^17.0.0",
23
+ "@atlaskit/editor-plugin-analytics": "^16.0.0",
24
+ "@atlaskit/editor-plugin-context-identifier": "^16.0.0",
25
+ "@atlaskit/editor-plugin-editor-viewmode": "^18.0.0",
26
26
  "@babel/runtime": "^7.0.0",
27
27
  "bind-event-listener": "^3.0.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "@atlaskit/editor-common": "^119.9.0",
30
+ "@atlaskit/editor-common": "^120.0.0",
31
31
  "react": "^18.2.0 || ^19.2.0"
32
32
  },
33
33
  "devDependencies": {