@atlaskit/editor-plugin-interactivity 0.2.0 → 0.3.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 +13 -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 +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @atlaskit/editor-plugin-interactivity
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`355d6f13eb8b4`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/355d6f13eb8b4) -
8
+ Report the `editorTyping`, `editorPointer` and `editorOther` interaction histograms in the
9
+ `editor interactivity` event, so a responsiveness regression in the editor can be told apart from
10
+ one elsewhere on the page.
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies
15
+
3
16
  ## 0.2.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -19,6 +19,10 @@ interactions were slow, so this plugin keeps bucketed counts instead.
19
19
  `buckets` and `percentilesMs`. `totalCount` comes from `performance.interactionCount` and includes
20
20
  interactions below the 16 ms Event Timing reporting threshold, so `totalCount - observedCount` is
21
21
  the sub-threshold count.
22
+ - `editorTyping`, `editorPointer` and `editorOther` — the interactions with the editor, in the same
23
+ shape as `page`. Event Timing observes the whole document, so `page` alone cannot say whether a
24
+ regression is in the editor or elsewhere on the page; these can. Every interaction with the editor
25
+ is in exactly one of them, and in `page` as well.
22
26
  - `percentilesMs` is temporary. It holds percentiles of the same interactions, keyed by percentile
23
27
  and exact to the 8 ms Event Timing reports durations at, to confirm that a percentile read off
24
28
  `buckets` lands where the latencies actually are.
@@ -28,6 +32,33 @@ interactions were slow, so this plugin keeps bucketed counts instead.
28
32
  - Snapshots are session-to-date, so a cohort query takes the highest `seq` per
29
33
  `interactivitySessionId` and then sums bucket counts.
30
34
 
35
+ ## The editor groups
36
+
37
+ An interaction is the editor's when it happened inside the element the editor renders itself into,
38
+ which editor-core hands to the plugin's hook as `wrapperElement`. A second observer reports the
39
+ events of those interactions from that element, alongside the Event Timing observer that reports the
40
+ latencies, and the tracker makes interactions out of both.
41
+
42
+ The editor's events are what count the interactions, because there is no
43
+ `performance.interactionCount` per group and the 16 ms reporting threshold is the lowest the spec
44
+ allows. An interaction is several events, so it is counted on one of them: typing on `keydown`,
45
+ because a held key repeats and the browser counts every repeat; pointing on `pointerup`, because a
46
+ press taken over by a scroll never gets one — and the browser counts no interaction for it either.
47
+ The group comes from the same events, and the one table in `interaction-events.ts` is read for both
48
+ counting and grouping, so the two cannot disagree.
49
+
50
+ An entry lands in the group of the event it measured, matched by that event's type and timestamp. An
51
+ interaction whose events the editor never reported stays in `page` only.
52
+
53
+ Known gaps, all of which only move interactions out of the editor groups and never between them:
54
+
55
+ - What the editor renders outside that element — a dropdown or a dialog in a portal — is counted in
56
+ `page` only, as is everything before the element arrives, a render after the editor first paints.
57
+ - `editorOther` is the remainder slot: the browser counts only keyboard and pointer interactions
58
+ today, so it reads zero until that changes.
59
+ - While an IME composes, the browser can group key presses into fewer interactions than we count, so
60
+ `editorTyping.totalCount` can run ahead of it.
61
+
31
62
  ## Cadence
32
63
 
33
64
  Snapshots are taken 10 s, 30 s and 60 s after the session starts, then every 60 s, plus on every
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.EditorEventObserver = void 0;
8
+ var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
9
+ var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
10
+ var _bindEventListener = require("bind-event-listener");
11
+ var _interactionEvents = require("./interaction-events");
12
+ /**
13
+ * Reports the events the interactions with the editor are made of, as the browser dispatched
14
+ * them. What they mean is the tracker's business.
15
+ *
16
+ * Event Timing cannot answer which interactions were with the editor, because it observes the
17
+ * whole document, nor how many there were, because it reports none below 16 ms.
18
+ */
19
+ var EditorEventObserver = exports.EditorEventObserver = /*#__PURE__*/function () {
20
+ function EditorEventObserver(onEvent) {
21
+ (0, _classCallCheck2.default)(this, EditorEventObserver);
22
+ this.onEvent = onEvent;
23
+ }
24
+
25
+ /**
26
+ * Called as the element the editor renders itself into changes: the editor only knows it after
27
+ * its first render, and can replace it.
28
+ */
29
+ return (0, _createClass2.default)(EditorEventObserver, [{
30
+ key: "observe",
31
+ value: function observe(root) {
32
+ var _this = this;
33
+ var next = root !== null && root !== void 0 ? root : undefined;
34
+ if (next === this.root) {
35
+ return;
36
+ }
37
+ this.stop();
38
+ this.root = next;
39
+ if (!next) {
40
+ return;
41
+ }
42
+ this.unbind = (0, _bindEventListener.bindAll)(next, _interactionEvents.INTERACTION_EVENT_TYPES.map(function (type) {
43
+ return {
44
+ type: type,
45
+ listener: _this.onEvent
46
+ };
47
+ }),
48
+ // Capture, so a handler in the editor cannot stop an interaction from being reported.
49
+ {
50
+ capture: true,
51
+ passive: true
52
+ });
53
+ }
54
+ }, {
55
+ key: "stop",
56
+ value: function stop() {
57
+ var _this$unbind;
58
+ (_this$unbind = this.unbind) === null || _this$unbind === void 0 || _this$unbind.call(this);
59
+ this.unbind = undefined;
60
+ this.root = undefined;
61
+ }
62
+ }]);
63
+ }();
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.INTERACTION_EVENT_TYPES = void 0;
7
+ exports.interactionEventKind = interactionEventKind;
8
+ /**
9
+ * The editor group an interaction belongs to, which is also the field the event reports it in.
10
+ *
11
+ * `editorOther` is the remainder — an interaction the browser counts that is neither typing nor
12
+ * pointing — so the three groups together cover whatever the browser calls an interaction.
13
+ */
14
+
15
+ /**
16
+ * The events an interaction is made of, and the group they belong to. Both sides of the collector
17
+ * read this, so counting and grouping cannot disagree.
18
+ *
19
+ * These are the events Event Timing can report with a non-zero `interactionId`, so any of them can
20
+ * be the one an entry measured — and it is usually not the one the interaction is counted on: for a
21
+ * pointer press the slow part is normally the `click` handler.
22
+ *
23
+ * The counted event is at a different end of the interaction in each group, because what can go
24
+ * wrong differs. Typing is counted on `keydown`, since the browser opens a new interaction for
25
+ * every repeat of a held key, and nothing cancels a key press. Pointing is counted on `pointerup`,
26
+ * since a press cannot repeat but can be taken over by a scroll — which the browser does not count
27
+ * either, and in that case `pointerup` never arrives.
28
+ */
29
+ var INTERACTION_EVENTS = {
30
+ keydown: {
31
+ group: 'editorTyping',
32
+ counts: true
33
+ },
34
+ keyup: {
35
+ group: 'editorTyping'
36
+ },
37
+ // Only while an IME composes: the browser counts the text it commits in the interaction of the
38
+ // key that caused it.
39
+ input: {
40
+ group: 'editorTyping'
41
+ },
42
+ pointerdown: {
43
+ group: 'editorPointer'
44
+ },
45
+ pointerup: {
46
+ group: 'editorPointer',
47
+ counts: true
48
+ },
49
+ click: {
50
+ group: 'editorPointer'
51
+ },
52
+ // Ends a pointer press the way `pointerup` does, so the browser counts it as an interaction.
53
+ contextmenu: {
54
+ group: 'editorPointer'
55
+ }
56
+ };
57
+
58
+ /** Derived, so the types listened for cannot drift from the table. */
59
+ var INTERACTION_EVENT_TYPES = exports.INTERACTION_EVENT_TYPES = Object.keys(INTERACTION_EVENTS);
60
+ function interactionEventKind(type) {
61
+ return INTERACTION_EVENTS[type];
62
+ }
@@ -30,17 +30,33 @@ var REPORTED_QUANTILES = [0.9, 0.98];
30
30
  * whatever its latency was, and everything the event carries — the reported buckets, the
31
31
  * count, the sum, the maximum and the percentiles — is derived from that map when a snapshot
32
32
  * is taken. Nothing is computed while interactions arrive.
33
+ *
34
+ * The two counters count different populations: `add` takes the interactions Event Timing
35
+ * measured, `countTotal` takes all of them, including the ones below the 16 ms reporting threshold
36
+ * it never delivers. So `totalCount >= observedCount`, and the difference is how many were too
37
+ * fast to be measured.
33
38
  */
34
39
  var InteractionGroup = exports.InteractionGroup = /*#__PURE__*/function () {
35
40
  function InteractionGroup() {
36
41
  (0, _classCallCheck2.default)(this, InteractionGroup);
37
42
  (0, _defineProperty2.default)(this, "countByLatency", new Map());
43
+ (0, _defineProperty2.default)(this, "totalCount", 0);
38
44
  }
39
45
  return (0, _createClass2.default)(InteractionGroup, [{
40
46
  key: "add",
41
47
  value: function add(latencyMs) {
42
48
  this.increment(latencyMs);
43
49
  }
50
+
51
+ /**
52
+ * Counts an interaction towards the group's total, measured or not. `page` has no use for it:
53
+ * `performance.interactionCount` counts the page's interactions.
54
+ */
55
+ }, {
56
+ key: "countTotal",
57
+ value: function countTotal() {
58
+ this.totalCount += 1;
59
+ }
44
60
  }, {
45
61
  key: "remeasure",
46
62
  value: function remeasure(previousLatencyMs, latencyMs) {
@@ -51,12 +67,14 @@ var InteractionGroup = exports.InteractionGroup = /*#__PURE__*/function () {
51
67
 
52
68
  /**
53
69
  * @param totalCount every interaction of the group, including those below the Event Timing
54
- * reporting threshold, which this group never sees.
70
+ * reporting threshold. Defaults to what `countTotal` was told, which is where an editor
71
+ * group's total comes from; `page` passes `performance.interactionCount` instead.
55
72
  */
56
73
  }, {
57
74
  key: "snapshot",
58
- value: function snapshot(totalCount) {
75
+ value: function snapshot() {
59
76
  var _latencies;
77
+ var totalCount = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.totalCount;
60
78
  // Ascending, so the reported buckets come out in order and the last latency is the
61
79
  // maximum. Sorted once for everything below.
62
80
  var latencies = Array.from(this.countByLatency.keys()).sort(function (a, b) {
@@ -8,6 +8,7 @@ exports.InteractionTracker = void 0;
8
8
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
9
9
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
10
10
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11
+ var _interactionEvents = require("./interaction-events");
11
12
  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; } } }; }
12
13
  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; } }
13
14
  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; }
@@ -20,12 +21,13 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
20
21
  /**
21
22
  * What an entry did to the interaction it belongs to: either it is the first entry of a new
22
23
  * interaction, or it measured an interaction that was already counted as slower than it was
23
- * known to be.
24
+ * known to be. Both carry the editor group of the interaction, if it is one of the editor's.
24
25
  */
25
26
 
26
27
  /**
27
- * How many interactions are remembered, so their growth can still be applied. Entries of one
28
- * interaction arrive within the interaction itself, so anything older is not needed.
28
+ * How many interactions are remembered, so their growth can still be applied, and how many of
29
+ * the editor's events. Entries of one interaction arrive within the interaction itself, so
30
+ * anything older than the last few hundred is not needed.
29
31
  */
30
32
  var MAX_TRACKED = 256;
31
33
  /**
@@ -35,7 +37,17 @@ var MAX_TRACKED = 256;
35
37
  var PRUNE_BATCH = 64;
36
38
 
37
39
  /**
38
- * Groups Event Timing entries into interactions.
40
+ * Identifies the event an entry measured: an entry's `startTime` is that event's timestamp and its
41
+ * `name` is its type. The type is in the key because browsers coarsen the timestamp, so two events
42
+ * of one task can share it — and events of one type are always of one group, so a collision cannot
43
+ * move an interaction into another group.
44
+ */
45
+ function eventKey(type, timeStamp) {
46
+ return "".concat(type, "|").concat(timeStamp);
47
+ }
48
+
49
+ /**
50
+ * Makes interactions out of what the two observers report, for one session.
39
51
  *
40
52
  * Entries sharing a non-zero `interactionId` are one interaction whose latency is the
41
53
  * maximum `duration` among them. Entries arrive incrementally, so an interaction's latency
@@ -45,6 +57,9 @@ var PRUNE_BATCH = 64;
45
57
  * A non-zero `interactionId` is the browser's own definition of an interaction, which is
46
58
  * also what INP filters on: it is assigned to the pointer and keyboard events that make one
47
59
  * up, and never to scrolling or pointer movement.
60
+ *
61
+ * The editor's events answer what an entry cannot: which interactions were with the editor, and
62
+ * how many there were, including the ones below the Event Timing reporting threshold.
48
63
  */
49
64
  var InteractionTracker = exports.InteractionTracker = /*#__PURE__*/function () {
50
65
  /**
@@ -57,7 +72,8 @@ var InteractionTracker = exports.InteractionTracker = /*#__PURE__*/function () {
57
72
  function InteractionTracker() {
58
73
  var startsAfterInteractionId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
59
74
  (0, _classCallCheck2.default)(this, InteractionTracker);
60
- (0, _defineProperty2.default)(this, "latencies", new Map());
75
+ (0, _defineProperty2.default)(this, "interactions", new Map());
76
+ (0, _defineProperty2.default)(this, "groupByEvent", new Map());
61
77
  (0, _defineProperty2.default)(this, "highestInteractionId", 0);
62
78
  this.startsAfterInteractionId = startsAfterInteractionId;
63
79
  }
@@ -94,41 +110,66 @@ var InteractionTracker = exports.InteractionTracker = /*#__PURE__*/function () {
94
110
  if (!Number.isFinite(entry.duration) || entry.duration < 0) {
95
111
  return undefined;
96
112
  }
97
- var previousLatencyMs = this.latencies.get(interactionId);
98
- if (previousLatencyMs === undefined) {
99
- this.latencies.set(interactionId, entry.duration);
100
- this.prune();
113
+ var tracked = this.interactions.get(interactionId);
114
+ if (tracked === undefined) {
115
+ // Taken once: an entry that only makes the interaction slower has to move its count
116
+ // within the group it was counted in, not into another one.
117
+ var group = this.groupByEvent.get(eventKey(entry.name, entry.startTime));
118
+ this.interactions.set(interactionId, {
119
+ latencyMs: entry.duration,
120
+ group: group
121
+ });
122
+ this.prune(this.interactions);
101
123
  return {
102
124
  type: 'new',
103
- latencyMs: entry.duration
125
+ latencyMs: entry.duration,
126
+ group: group
104
127
  };
105
128
  }
106
- if (entry.duration <= previousLatencyMs) {
129
+ if (entry.duration <= tracked.latencyMs) {
107
130
  return undefined;
108
131
  }
109
- this.latencies.set(interactionId, entry.duration);
132
+ var fromMs = tracked.latencyMs;
133
+ tracked.latencyMs = entry.duration;
110
134
  return {
111
135
  type: 'remeasured',
112
- fromMs: previousLatencyMs,
113
- toMs: entry.duration
136
+ fromMs: fromMs,
137
+ toMs: entry.duration,
138
+ group: tracked.group
114
139
  };
115
140
  }
141
+
142
+ /** @returns the group of an interaction to count, when this is the event its group counts on. */
143
+ }, {
144
+ key: "recordEditorEvent",
145
+ value: function recordEditorEvent(event) {
146
+ var kind = (0, _interactionEvents.interactionEventKind)(event.type);
147
+ if (!kind) {
148
+ return undefined;
149
+ }
150
+
151
+ // Every event of the interaction, because any of them can be the one Event Timing reports
152
+ // as the slowest: for a pointer press that is usually the click.
153
+ this.groupByEvent.set(eventKey(event.type, event.timeStamp), kind.group);
154
+ this.prune(this.groupByEvent);
155
+ return kind.counts ? kind.group : undefined;
156
+ }
116
157
  }, {
117
158
  key: "prune",
118
- value: function prune() {
119
- if (this.latencies.size <= MAX_TRACKED) {
159
+ value: function prune(entries) {
160
+ if (entries.size <= MAX_TRACKED) {
120
161
  return;
121
162
  }
122
163
 
123
- // `Map` preserves insertion order and `interactionId` increases monotonically,
124
- // so the entries inserted first are the least likely to see another entry.
164
+ // `Map` preserves insertion order, so the entries inserted first are the least likely to
165
+ // see another entry or another event.
125
166
  var remaining = PRUNE_BATCH;
126
- var _iterator = _createForOfIteratorHelper(this.latencies.keys()),
167
+ var _iterator = _createForOfIteratorHelper(entries.keys()),
127
168
  _step;
128
169
  try {
129
170
  for (_iterator.s(); !(_step = _iterator.n()).done;) {
130
- var interactionId = _step.value;
131
- this.latencies.delete(interactionId);
171
+ var key = _step.value;
172
+ entries.delete(key);
132
173
  remaining -= 1;
133
174
  if (remaining === 0) {
134
175
  return;
@@ -9,6 +9,7 @@ var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/cl
9
9
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
10
10
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11
11
  var _browserApis = require("@atlaskit/browser-apis");
12
+ var _editorEventObserver = require("./editor-event-observer");
12
13
  var _interactionObserver = require("./interaction-observer");
13
14
  var _interactivitySession = require("./interactivity-session");
14
15
  var _bucketBoundaries = require("./bucket-boundaries");
@@ -20,6 +21,9 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
20
21
  /**
21
22
  * Collects interaction latencies for one editor mount and emits session-to-date snapshots.
22
23
  *
24
+ * Every interaction is counted in `page`, and the ones with the editor again in one of the editor
25
+ * groups, which is what tells a slow editor apart from a slow page around it.
26
+ *
23
27
  * `start` and `stop` bound the collecting, which happens once. Within it there can be several
24
28
  * sessions, because a session covers one document in one mode: a Confluence live page
25
29
  * navigates and switches between reading and editing without ever remounting the editor, and
@@ -39,6 +43,9 @@ var InteractivityCollector = exports.InteractivityCollector = /*#__PURE__*/funct
39
43
  this.interactionObserver = new _interactionObserver.InteractionObserver(function (entries) {
40
44
  return _this.recordEntries(entries);
41
45
  });
46
+ this.editorEvents = new _editorEventObserver.EditorEventObserver(function (event) {
47
+ return _this.recordEditorEvent(event);
48
+ });
42
49
  this.snapshotScheduler = new _snapshotScheduler.SnapshotScheduler(function () {
43
50
  return _this.onTimer();
44
51
  });
@@ -83,6 +90,7 @@ var InteractivityCollector = exports.InteractivityCollector = /*#__PURE__*/funct
83
90
  // this object was constructed.
84
91
  this.session = this.createSession(0);
85
92
  this.interactionObserver.start();
93
+ this.editorEvents.observe(this.editorRoot);
86
94
  this.snapshotScheduler.start();
87
95
  this.lifecycleObserver.start();
88
96
  this.started = true;
@@ -99,10 +107,24 @@ var InteractivityCollector = exports.InteractivityCollector = /*#__PURE__*/funct
99
107
  this.takeSnapshot('unmount');
100
108
  this.stopped = true;
101
109
  this.interactionObserver.stop();
110
+ this.editorEvents.stop();
102
111
  this.snapshotScheduler.stop();
103
112
  this.lifecycleObserver.stop();
104
113
  }
105
114
 
115
+ /**
116
+ * The element the editor renders itself into is what makes an interaction one of the editor's.
117
+ * The session is not tied to it: interactions from before it arrives are counted in `page`.
118
+ */
119
+ }, {
120
+ key: "setEditorRoot",
121
+ value: function setEditorRoot(root) {
122
+ this.editorRoot = root !== null && root !== void 0 ? root : undefined;
123
+ if (this.started && !this.stopped) {
124
+ this.editorEvents.observe(this.editorRoot);
125
+ }
126
+ }
127
+
106
128
  /**
107
129
  * Rotates the session when the editor is pointed at different content.
108
130
  *
@@ -212,6 +234,26 @@ var InteractivityCollector = exports.InteractivityCollector = /*#__PURE__*/funct
212
234
  value: function onPageShow() {
213
235
  this.session.lifecycleSnapshotEmitted = false;
214
236
  }
237
+
238
+ /**
239
+ * Counts an interaction with the editor, including the ones Event Timing never reports because
240
+ * they were faster than its threshold — which is why a count alone moves the session on.
241
+ */
242
+ }, {
243
+ key: "recordEditorEvent",
244
+ value: function recordEditorEvent(event) {
245
+ if (this.stopped) {
246
+ return;
247
+ }
248
+ var group = this.session.tracker.recordEditorEvent(event);
249
+ if (!group) {
250
+ return;
251
+ }
252
+
253
+ // The session names its editor groups after the fields they are reported in.
254
+ this.session[group].countTotal();
255
+ this.session.revision += 1;
256
+ }
215
257
  }, {
216
258
  key: "recordEntries",
217
259
  value: function recordEntries(entries) {
@@ -229,8 +271,14 @@ var InteractivityCollector = exports.InteractivityCollector = /*#__PURE__*/funct
229
271
  }
230
272
  if (update.type === 'new') {
231
273
  page.add(update.latencyMs);
274
+ if (update.group) {
275
+ this.session[update.group].add(update.latencyMs);
276
+ }
232
277
  } else {
233
278
  page.remeasure(update.fromMs, update.toMs);
279
+ if (update.group) {
280
+ this.session[update.group].remeasure(update.fromMs, update.toMs);
281
+ }
234
282
  }
235
283
  this.session.revision += 1;
236
284
  }
@@ -270,7 +318,7 @@ var InteractivityCollector = exports.InteractivityCollector = /*#__PURE__*/funct
270
318
  session.emittedRevision = session.revision;
271
319
  var now = performance.now();
272
320
  var hiddenMs = session.hiddenMs + (session.hiddenSince === undefined ? 0 : now - session.hiddenSince);
273
- var totalCount = _interactionObserver.InteractionObserver.readPageInteractionCount() - session.interactionCountAtStart;
321
+ var pageTotalCount = _interactionObserver.InteractionObserver.readPageInteractionCount() - session.interactionCountAtStart;
274
322
  this.emit({
275
323
  schema: _bucketBoundaries.SCHEMA_VERSION,
276
324
  interactivitySessionId: session.id,
@@ -282,7 +330,11 @@ var InteractivityCollector = exports.InteractivityCollector = /*#__PURE__*/funct
282
330
  hiddenMs: Math.round(hiddenMs),
283
331
  nodeSize: this.getNodeSize(),
284
332
  editorDomSize: this.getEditorDomSize(),
285
- page: session.page.snapshot(totalCount)
333
+ // Only `page` is told its total; each editor group has counted its own.
334
+ page: session.page.snapshot(pageTotalCount),
335
+ editorTyping: session.editorTyping.snapshot(),
336
+ editorPointer: session.editorPointer.snapshot(),
337
+ editorOther: session.editorOther.snapshot()
286
338
  });
287
339
  }
288
340
  }]);
@@ -34,6 +34,9 @@ var InteractivitySession = exports.InteractivitySession = /*#__PURE__*/(0, _crea
34
34
  /** Page interaction count when the session opened, subtracted to get its own total. */
35
35
  (0, _defineProperty2.default)(this, "interactionCountAtStart", _interactionObserver.InteractionObserver.readPageInteractionCount());
36
36
  (0, _defineProperty2.default)(this, "page", new _interactionGroup.InteractionGroup());
37
+ (0, _defineProperty2.default)(this, "editorTyping", new _interactionGroup.InteractionGroup());
38
+ (0, _defineProperty2.default)(this, "editorPointer", new _interactionGroup.InteractionGroup());
39
+ (0, _defineProperty2.default)(this, "editorOther", new _interactionGroup.InteractionGroup());
37
40
  /** Increments per snapshot; a query takes the highest one per session. */
38
41
  (0, _defineProperty2.default)(this, "seq", 0);
39
42
  (0, _defineProperty2.default)(this, "hiddenMs", 0);
@@ -22,11 +22,10 @@ var interactivityPlugin = exports.interactivityPlugin = function interactivityPl
22
22
  return {
23
23
  name: 'interactivity',
24
24
  usePluginHook: function usePluginHook(_ref2) {
25
- var editorView = _ref2.editorView;
25
+ var editorView = _ref2.editorView,
26
+ wrapperElement = _ref2.wrapperElement;
27
+ var collectorRef = (0, _react.useRef)(undefined);
26
28
  (0, _react.useEffect)(function () {
27
- // Nothing measured here is worth an editor. This effect runs inside the plugin slot's
28
- // error boundary, which also renders every plugin's content components, so a throw
29
- // from instrumentation would take that UI down with it.
30
29
  try {
31
30
  var _api$contextIdentifie2, _api$editorViewMode2;
32
31
  var collector = new _interactivityCollector.InteractivityCollector({
@@ -57,6 +56,9 @@ var interactivityPlugin = exports.interactivityPlugin = function interactivityPl
57
56
  return editorView.isDestroyed ? undefined : editorView.dom.getElementsByTagName('*').length;
58
57
  }
59
58
  });
59
+
60
+ // Effects run in declaration order, so the one below always finds it.
61
+ collectorRef.current = collector;
60
62
  if (!collector.start()) {
61
63
  return;
62
64
  }
@@ -88,6 +90,16 @@ var interactivityPlugin = exports.interactivityPlugin = function interactivityPl
88
90
  // live registry, and restarting the session on a reconfigure would split one
89
91
  // editor session in two.
90
92
  }, [editorView]);
93
+ (0, _react.useEffect)(function () {
94
+ try {
95
+ var _collectorRef$current;
96
+ (_collectorRef$current = collectorRef.current) === null || _collectorRef$current === void 0 || _collectorRef$current.setEditorRoot(wrapperElement);
97
+ } catch (error) {
98
+ void (0, _monitoring.logException)(error, {
99
+ location: 'editor-plugin-interactivity/observe'
100
+ });
101
+ }
102
+ }, [wrapperElement]);
91
103
  }
92
104
  };
93
105
  };
@@ -0,0 +1,46 @@
1
+ import { bindAll } from 'bind-event-listener';
2
+ import { INTERACTION_EVENT_TYPES } from './interaction-events';
3
+
4
+ /**
5
+ * Reports the events the interactions with the editor are made of, as the browser dispatched
6
+ * them. What they mean is the tracker's business.
7
+ *
8
+ * Event Timing cannot answer which interactions were with the editor, because it observes the
9
+ * whole document, nor how many there were, because it reports none below 16 ms.
10
+ */
11
+ export class EditorEventObserver {
12
+ constructor(onEvent) {
13
+ this.onEvent = onEvent;
14
+ }
15
+
16
+ /**
17
+ * Called as the element the editor renders itself into changes: the editor only knows it after
18
+ * its first render, and can replace it.
19
+ */
20
+ observe(root) {
21
+ const next = root !== null && root !== void 0 ? root : undefined;
22
+ if (next === this.root) {
23
+ return;
24
+ }
25
+ this.stop();
26
+ this.root = next;
27
+ if (!next) {
28
+ return;
29
+ }
30
+ this.unbind = bindAll(next, INTERACTION_EVENT_TYPES.map(type => ({
31
+ type,
32
+ listener: this.onEvent
33
+ })),
34
+ // Capture, so a handler in the editor cannot stop an interaction from being reported.
35
+ {
36
+ capture: true,
37
+ passive: true
38
+ });
39
+ }
40
+ stop() {
41
+ var _this$unbind;
42
+ (_this$unbind = this.unbind) === null || _this$unbind === void 0 ? void 0 : _this$unbind.call(this);
43
+ this.unbind = undefined;
44
+ this.root = undefined;
45
+ }
46
+ }
@@ -0,0 +1,55 @@
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
+
8
+ /**
9
+ * The events an interaction is made of, and the group they belong to. Both sides of the collector
10
+ * read this, so counting and grouping cannot disagree.
11
+ *
12
+ * These are the events Event Timing can report with a non-zero `interactionId`, so any of them can
13
+ * be the one an entry measured — and it is usually not the one the interaction is counted on: for a
14
+ * pointer press the slow part is normally the `click` handler.
15
+ *
16
+ * The counted event is at a different end of the interaction in each group, because what can go
17
+ * wrong differs. Typing is counted on `keydown`, since the browser opens a new interaction for
18
+ * every repeat of a held key, and nothing cancels a key press. Pointing is counted on `pointerup`,
19
+ * since a press cannot repeat but can be taken over by a scroll — which the browser does not count
20
+ * either, and in that case `pointerup` never arrives.
21
+ */
22
+ const INTERACTION_EVENTS = {
23
+ keydown: {
24
+ group: 'editorTyping',
25
+ counts: true
26
+ },
27
+ keyup: {
28
+ group: 'editorTyping'
29
+ },
30
+ // Only while an IME composes: the browser counts the text it commits in the interaction of the
31
+ // key that caused it.
32
+ input: {
33
+ group: 'editorTyping'
34
+ },
35
+ pointerdown: {
36
+ group: 'editorPointer'
37
+ },
38
+ pointerup: {
39
+ group: 'editorPointer',
40
+ counts: true
41
+ },
42
+ click: {
43
+ group: 'editorPointer'
44
+ },
45
+ // Ends a pointer press the way `pointerup` does, so the browser counts it as an interaction.
46
+ contextmenu: {
47
+ group: 'editorPointer'
48
+ }
49
+ };
50
+
51
+ /** Derived, so the types listened for cannot drift from the table. */
52
+ export const INTERACTION_EVENT_TYPES = Object.keys(INTERACTION_EVENTS);
53
+ export function interactionEventKind(type) {
54
+ return INTERACTION_EVENTS[type];
55
+ }