@atlaskit/editor-plugin-interactivity 1.2.0 → 1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @atlaskit/editor-plugin-interactivity
2
2
 
3
+ ## 1.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`49ec73019afa7`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/49ec73019afa7) -
8
+ Add an `editor` group to the `editor interactivity` event: the three editor groups as one, in the
9
+ same shape, so the session p98 of the editor as a whole can be read from `editor.percentilesMs`.
10
+
11
+ ### Patch Changes
12
+
13
+ - [`58e0ed4f4c98f`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/58e0ed4f4c98f) -
14
+ Rank `percentilesMs` in the `editor interactivity` event over `totalCount` rather than
15
+ `observedCount`, so the interactions below the Event Timing reporting threshold count towards the
16
+ percentile as they do for INP. A percentile that falls among them is reported as the
17
+ threshold, 16.
18
+
19
+ ## 1.2.1
20
+
21
+ ### Patch Changes
22
+
23
+ - [`f1f320e43a22e`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/f1f320e43a22e) -
24
+ Ignore untrusted events in the `editor interactivity` collector, so events a script dispatched are
25
+ no longer counted as interactions with the editor.
26
+ - Updated dependencies
27
+
3
28
  ## 1.2.0
4
29
 
5
30
  ### Minor Changes
package/README.md CHANGED
@@ -10,22 +10,24 @@ be able to use this component but will not be able to submit issues.
10
10
  The Interactivity plugin reports the `editor interactivity` operational event: session-to-date
11
11
  interaction latency distributions for full page editor sessions, per
12
12
  [RFC 095](https://hello.atlassian.net/wiki/spaces/EDITOR/pages/7527607488/Editor+RFC+095+Confluence+editor+responsiveness+bucketed+INP+telemetry).
13
- The existing `editor inp` event reports a single value per session and cannot answer how many
14
- interactions were slow, so this plugin keeps bucketed counts instead.
13
+ Signals that report one value per session cannot answer how many interactions were slow, so this
14
+ plugin keeps bucketed counts instead.
15
15
 
16
16
  ## What it reports
17
17
 
18
18
  - `page` — every interaction on the page, as `totalCount`, `observedCount`, `sumMs`, `maxMs`,
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
- the sub-threshold count.
21
+ the sub-threshold count. `percentilesMs` ranks over `totalCount`, as INP does, so a percentile that
22
+ falls among the sub-threshold interactions reads as 16.
22
23
  - `editorTyping`, `editorPointer` and `editorOther` — the interactions with the editor, in the same
23
24
  shape as `page`. Event Timing observes the whole document, so `page` alone cannot say whether a
24
25
  regression is in the editor or elsewhere on the page; these can. Every interaction with the editor
25
26
  is in exactly one of them, and in `page` as well.
26
- - `percentilesMs` is temporary. It holds percentiles of the same interactions, keyed by percentile
27
- and exact to the 8 ms Event Timing reports durations at, to confirm that a percentile read off
28
- `buckets` lands where the latencies actually are.
27
+ - `editor` the three editor groups as one, in the same shape: the per-session p98 of the editor as
28
+ a whole, which theirs cannot give.
29
+ - `percentilesMs` percentiles of the same interactions, keyed by percentile and exact to the 8 ms
30
+ Event Timing reports durations at; a per-session metric reads these rather than `buckets`.
29
31
  - Bucket keys are the upper boundary of the bucket in milliseconds and count only interactions above
30
32
  the previous boundary. Buckets are not cumulative and empty buckets are omitted, so a missing
31
33
  bucket means zero. The boundaries are versioned by `schema`.
@@ -18,6 +18,10 @@ exports.interactionEventKind = interactionEventKind;
18
18
  * every repeat of a held key, and nothing cancels a key press. Pointing is counted on `pointerup`,
19
19
  * since a press cannot repeat but can be taken over by a scroll — which the browser does not count
20
20
  * either, and in that case `pointerup` never arrives.
21
+ *
22
+ * Counting events is also why an editor group's `totalCount` is not comparable with `page`'s, which
23
+ * is told `performance.interactionCount`: when an event is counted the collector cannot know whether
24
+ * the browser will open an interaction for it.
21
25
  */
22
26
  var INTERACTION_EVENTS = {
23
27
  keydown: {
@@ -102,28 +102,73 @@ var InteractionGroup = exports.InteractionGroup = /*#__PURE__*/function () {
102
102
  value: function snapshot() {
103
103
  var _latencies;
104
104
  var totalCount = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.totalCount;
105
+ var observedCount = this.observedCount();
106
+ // `performance.interactionCount` can lag the entries the observer has delivered.
107
+ var reportedTotalCount = Math.max(totalCount, observedCount);
108
+ if (reportedTotalCount === 0) {
109
+ return {
110
+ totalCount: 0,
111
+ observedCount: 0,
112
+ sumMs: 0,
113
+ maxMs: 0,
114
+ buckets: {},
115
+ percentilesMs: {}
116
+ };
117
+ }
118
+
105
119
  // Ascending, so the reported buckets come out in order and the last latency is the
106
120
  // maximum. Sorted once for everything below.
107
121
  var latencies = Array.from(this.countByLatency.keys()).sort(function (a, b) {
108
122
  return a - b;
109
123
  });
110
- var observedCount = this.observedCount();
124
+
125
+ // A percentile is the interaction at position `ceil(quantile * total)` of all interactions
126
+ // sorted by latency. Event Timing never reports the ones under 16 ms, but they are the fastest,
127
+ // so they fill the front of the line, and the rank is the position among the measured ones:
128
+ //
129
+ // 250 interactions, 150 of them unmeasured
130
+ //
131
+ // position 1 ........... 150 | 151 ........... 245 .... 250
132
+ // latency unmeasured, < 16 ms | 16 ms ............. measured
133
+ // ^ p98
134
+ //
135
+ // p98 is position ceil(0.98 * 250) = 245 of all, rank 245 - 150 = 95 among the measured.
136
+ var unmeasuredCount = reportedTotalCount - observedCount;
111
137
  var percentileRanks = REPORTED_QUANTILES.map(function (quantile) {
112
138
  return {
113
139
  key: String(Math.round(quantile * 100)),
114
- rank: Math.max(1, Math.ceil(quantile * observedCount))
140
+ rank: Math.ceil(quantile * reportedTotalCount) - unmeasuredCount
115
141
  };
116
142
  });
117
143
  var buckets = {};
118
144
  var percentilesMs = {};
119
145
  var sumMs = 0;
120
146
  var counted = 0;
121
- var _iterator = _createForOfIteratorHelper(latencies),
147
+
148
+ // A rank of 0 or below is a latency Event Timing never reports, so the percentile gets the
149
+ // lowest one it does.
150
+ var _iterator = _createForOfIteratorHelper(percentileRanks),
122
151
  _step;
123
152
  try {
124
153
  for (_iterator.s(); !(_step = _iterator.n()).done;) {
154
+ var _step$value = _step.value,
155
+ key = _step$value.key,
156
+ rank = _step$value.rank;
157
+ if (rank <= 0) {
158
+ percentilesMs[key] = _bucketBoundaries.REPORTING_THRESHOLD_MS;
159
+ }
160
+ }
161
+ } catch (err) {
162
+ _iterator.e(err);
163
+ } finally {
164
+ _iterator.f();
165
+ }
166
+ var _iterator2 = _createForOfIteratorHelper(latencies),
167
+ _step2;
168
+ try {
169
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
125
170
  var _this$countByLatency$, _buckets$bucket;
126
- var latencyMs = _step.value;
171
+ var latencyMs = _step2.value;
127
172
  var count = (_this$countByLatency$ = this.countByLatency.get(latencyMs)) !== null && _this$countByLatency$ !== void 0 ? _this$countByLatency$ : 0;
128
173
  sumMs += latencyMs * count;
129
174
  var bucket = String((0, _bucketBoundaries.bucketKeyForMs)(latencyMs));
@@ -132,31 +177,30 @@ var InteractionGroup = exports.InteractionGroup = /*#__PURE__*/function () {
132
177
  // A percentile is the latency the group's interactions reach counting up from the
133
178
  // fastest, so it is answered as soon as this many of them have been passed.
134
179
  counted += count;
135
- var _iterator2 = _createForOfIteratorHelper(percentileRanks),
136
- _step2;
180
+ var _iterator3 = _createForOfIteratorHelper(percentileRanks),
181
+ _step3;
137
182
  try {
138
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
139
- var _step2$value = _step2.value,
140
- key = _step2$value.key,
141
- rank = _step2$value.rank;
142
- if (percentilesMs[key] === undefined && counted >= rank) {
143
- percentilesMs[key] = latencyMs;
183
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
184
+ var _step3$value = _step3.value,
185
+ _key = _step3$value.key,
186
+ _rank = _step3$value.rank;
187
+ if (percentilesMs[_key] === undefined && counted >= _rank) {
188
+ percentilesMs[_key] = latencyMs;
144
189
  }
145
190
  }
146
191
  } catch (err) {
147
- _iterator2.e(err);
192
+ _iterator3.e(err);
148
193
  } finally {
149
- _iterator2.f();
194
+ _iterator3.f();
150
195
  }
151
196
  }
152
197
  } catch (err) {
153
- _iterator.e(err);
198
+ _iterator2.e(err);
154
199
  } finally {
155
- _iterator.f();
200
+ _iterator2.f();
156
201
  }
157
202
  return {
158
- // `performance.interactionCount` can lag the entries the observer has delivered.
159
- totalCount: Math.max(totalCount, observedCount),
203
+ totalCount: reportedTotalCount,
160
204
  observedCount: observedCount,
161
205
  sumMs: sumMs,
162
206
  maxMs: (_latencies = latencies[latencies.length - 1]) !== null && _latencies !== void 0 ? _latencies : 0,
@@ -168,17 +212,17 @@ var InteractionGroup = exports.InteractionGroup = /*#__PURE__*/function () {
168
212
  key: "observedCount",
169
213
  value: function observedCount() {
170
214
  var total = 0;
171
- var _iterator3 = _createForOfIteratorHelper(this.countByLatency.values()),
172
- _step3;
215
+ var _iterator4 = _createForOfIteratorHelper(this.countByLatency.values()),
216
+ _step4;
173
217
  try {
174
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
175
- var count = _step3.value;
218
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
219
+ var count = _step4.value;
176
220
  total += count;
177
221
  }
178
222
  } catch (err) {
179
- _iterator3.e(err);
223
+ _iterator4.e(err);
180
224
  } finally {
181
- _iterator3.f();
225
+ _iterator4.f();
182
226
  }
183
227
  return total;
184
228
  }
@@ -25,8 +25,8 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
25
25
  /**
26
26
  * Collects interaction latencies for one editor mount and emits session-to-date snapshots.
27
27
  *
28
- * Every interaction is counted in `page`, and the ones with the editor again in one of the editor
29
- * groups, which is what tells a slow editor apart from a slow page around it.
28
+ * Every interaction is counted in `page`, and the ones with the editor again in `editor` and in one
29
+ * of the editor groups, which is what tells a slow editor apart from a slow page around it.
30
30
  *
31
31
  * `start` and `stop` bound the collecting, which happens once. Within it there can be several
32
32
  * sessions, because a session covers one document in one mode: a Confluence live page
@@ -256,11 +256,16 @@ var InteractivityCollector = exports.InteractivityCollector = /*#__PURE__*/funct
256
256
  if (this.stopped) {
257
257
  return;
258
258
  }
259
+
260
+ // An event a script dispatched is no interaction: the browser counts none of them either.
261
+ if (!event.isTrusted) {
262
+ return;
263
+ }
259
264
  var group = this.session.tracker.recordEditorEvent(event);
260
265
  if (!group) {
261
266
  return;
262
267
  }
263
-
268
+ this.session.editor.countTotal();
264
269
  // The session names its editor groups after the fields they are reported in.
265
270
  this.session[group].countTotal();
266
271
  this.session.revision += 1;
@@ -294,6 +299,7 @@ var InteractivityCollector = exports.InteractivityCollector = /*#__PURE__*/funct
294
299
  var update = _step2.value;
295
300
  var groupsChanged = this.session.page.trackInteractionUpdate(update);
296
301
  if (update.group) {
302
+ groupsChanged = this.session.editor.trackInteractionUpdate(update) || groupsChanged;
297
303
  groupsChanged = this.session[update.group].trackInteractionUpdate(update) || groupsChanged;
298
304
  }
299
305
  var slowestChanged = (_this$session$slowest2 = this.session.slowest) === null || _this$session$slowest2 === void 0 ? void 0 : _this$session$slowest2.trackInteractionUpdate(entry, update);
@@ -360,6 +366,7 @@ var InteractivityCollector = exports.InteractivityCollector = /*#__PURE__*/funct
360
366
  editorDomSize: this.getEditorDomSize(),
361
367
  // Only `page` is told its total; each editor group has counted its own.
362
368
  page: session.page.snapshot(pageTotalCount),
369
+ editor: session.editor.snapshot(),
363
370
  editorTyping: session.editorTyping.snapshot(),
364
371
  editorPointer: session.editorPointer.snapshot(),
365
372
  editorOther: session.editorOther.snapshot()
@@ -36,6 +36,7 @@ var InteractivitySession = exports.InteractivitySession = /*#__PURE__*/(0, _crea
36
36
  /** Page interaction count when the session opened, subtracted to get its own total. */
37
37
  (0, _defineProperty2.default)(this, "interactionCountAtStart", _interactionObserver.InteractionObserver.readPageInteractionCount());
38
38
  (0, _defineProperty2.default)(this, "page", new _interactionGroup.InteractionGroup());
39
+ (0, _defineProperty2.default)(this, "editor", new _interactionGroup.InteractionGroup());
39
40
  (0, _defineProperty2.default)(this, "editorTyping", new _interactionGroup.InteractionGroup());
40
41
  (0, _defineProperty2.default)(this, "editorPointer", new _interactionGroup.InteractionGroup());
41
42
  (0, _defineProperty2.default)(this, "editorOther", new _interactionGroup.InteractionGroup());
@@ -11,6 +11,10 @@
11
11
  * every repeat of a held key, and nothing cancels a key press. Pointing is counted on `pointerup`,
12
12
  * since a press cannot repeat but can be taken over by a scroll — which the browser does not count
13
13
  * either, and in that case `pointerup` never arrives.
14
+ *
15
+ * Counting events is also why an editor group's `totalCount` is not comparable with `page`'s, which
16
+ * is told `performance.interactionCount`: when an event is counted the collector cannot know whether
17
+ * the browser will open an interaction for it.
14
18
  */
15
19
  const INTERACTION_EVENTS = {
16
20
  keydown: {
@@ -1,5 +1,5 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
- import { bucketKeyForMs } from './bucket-boundaries';
2
+ import { bucketKeyForMs, REPORTING_THRESHOLD_MS } from './bucket-boundaries';
3
3
  /**
4
4
  * Latencies are counted per 8 ms, the resolution Event Timing reports durations at. Every
5
5
  * latency is rounded up to this step on the way in, which caps the number of distinct values
@@ -77,18 +77,55 @@ export class InteractionGroup {
77
77
  */
78
78
  snapshot(totalCount = this.totalCount) {
79
79
  var _latencies;
80
+ const observedCount = this.observedCount();
81
+ // `performance.interactionCount` can lag the entries the observer has delivered.
82
+ const reportedTotalCount = Math.max(totalCount, observedCount);
83
+ if (reportedTotalCount === 0) {
84
+ return {
85
+ totalCount: 0,
86
+ observedCount: 0,
87
+ sumMs: 0,
88
+ maxMs: 0,
89
+ buckets: {},
90
+ percentilesMs: {}
91
+ };
92
+ }
93
+
80
94
  // Ascending, so the reported buckets come out in order and the last latency is the
81
95
  // maximum. Sorted once for everything below.
82
96
  const latencies = Array.from(this.countByLatency.keys()).sort((a, b) => a - b);
83
- const observedCount = this.observedCount();
97
+
98
+ // A percentile is the interaction at position `ceil(quantile * total)` of all interactions
99
+ // sorted by latency. Event Timing never reports the ones under 16 ms, but they are the fastest,
100
+ // so they fill the front of the line, and the rank is the position among the measured ones:
101
+ //
102
+ // 250 interactions, 150 of them unmeasured
103
+ //
104
+ // position 1 ........... 150 | 151 ........... 245 .... 250
105
+ // latency unmeasured, < 16 ms | 16 ms ............. measured
106
+ // ^ p98
107
+ //
108
+ // p98 is position ceil(0.98 * 250) = 245 of all, rank 245 - 150 = 95 among the measured.
109
+ const unmeasuredCount = reportedTotalCount - observedCount;
84
110
  const percentileRanks = REPORTED_QUANTILES.map(quantile => ({
85
111
  key: String(Math.round(quantile * 100)),
86
- rank: Math.max(1, Math.ceil(quantile * observedCount))
112
+ rank: Math.ceil(quantile * reportedTotalCount) - unmeasuredCount
87
113
  }));
88
114
  const buckets = {};
89
115
  const percentilesMs = {};
90
116
  let sumMs = 0;
91
117
  let counted = 0;
118
+
119
+ // A rank of 0 or below is a latency Event Timing never reports, so the percentile gets the
120
+ // lowest one it does.
121
+ for (const {
122
+ key,
123
+ rank
124
+ } of percentileRanks) {
125
+ if (rank <= 0) {
126
+ percentilesMs[key] = REPORTING_THRESHOLD_MS;
127
+ }
128
+ }
92
129
  for (const latencyMs of latencies) {
93
130
  var _this$countByLatency$, _buckets$bucket;
94
131
  const count = (_this$countByLatency$ = this.countByLatency.get(latencyMs)) !== null && _this$countByLatency$ !== void 0 ? _this$countByLatency$ : 0;
@@ -109,8 +146,7 @@ export class InteractionGroup {
109
146
  }
110
147
  }
111
148
  return {
112
- // `performance.interactionCount` can lag the entries the observer has delivered.
113
- totalCount: Math.max(totalCount, observedCount),
149
+ totalCount: reportedTotalCount,
114
150
  observedCount,
115
151
  sumMs,
116
152
  maxMs: (_latencies = latencies[latencies.length - 1]) !== null && _latencies !== void 0 ? _latencies : 0,
@@ -11,8 +11,8 @@ import { SnapshotScheduler } from './snapshot-scheduler';
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.
14
+ * Every interaction is counted in `page`, and the ones with the editor again in `editor` and in one
15
+ * of the editor groups, which is what tells a slow editor apart from a slow page around it.
16
16
  *
17
17
  * `start` and `stop` bound the collecting, which happens once. Within it there can be several
18
18
  * sessions, because a session covers one document in one mode: a Confluence live page
@@ -198,11 +198,16 @@ export class InteractivityCollector {
198
198
  if (this.stopped) {
199
199
  return;
200
200
  }
201
+
202
+ // An event a script dispatched is no interaction: the browser counts none of them either.
203
+ if (!event.isTrusted) {
204
+ return;
205
+ }
201
206
  const group = this.session.tracker.recordEditorEvent(event);
202
207
  if (!group) {
203
208
  return;
204
209
  }
205
-
210
+ this.session.editor.countTotal();
206
211
  // The session names its editor groups after the fields they are reported in.
207
212
  this.session[group].countTotal();
208
213
  this.session.revision += 1;
@@ -224,6 +229,7 @@ export class InteractivityCollector {
224
229
  var _this$session$slowest2;
225
230
  let groupsChanged = this.session.page.trackInteractionUpdate(update);
226
231
  if (update.group) {
232
+ groupsChanged = this.session.editor.trackInteractionUpdate(update) || groupsChanged;
227
233
  groupsChanged = this.session[update.group].trackInteractionUpdate(update) || groupsChanged;
228
234
  }
229
235
  const slowestChanged = (_this$session$slowest2 = this.session.slowest) === null || _this$session$slowest2 === void 0 ? void 0 : _this$session$slowest2.trackInteractionUpdate(entry, update);
@@ -278,6 +284,7 @@ export class InteractivityCollector {
278
284
  editorDomSize: this.getEditorDomSize(),
279
285
  // Only `page` is told its total; each editor group has counted its own.
280
286
  page: session.page.snapshot(pageTotalCount),
287
+ editor: session.editor.snapshot(),
281
288
  editorTyping: session.editorTyping.snapshot(),
282
289
  editorPointer: session.editorPointer.snapshot(),
283
290
  editorOther: session.editorOther.snapshot(),
@@ -25,6 +25,7 @@ export class InteractivitySession {
25
25
  /** Page interaction count when the session opened, subtracted to get its own total. */
26
26
  _defineProperty(this, "interactionCountAtStart", InteractionObserver.readPageInteractionCount());
27
27
  _defineProperty(this, "page", new InteractionGroup());
28
+ _defineProperty(this, "editor", new InteractionGroup());
28
29
  _defineProperty(this, "editorTyping", new InteractionGroup());
29
30
  _defineProperty(this, "editorPointer", new InteractionGroup());
30
31
  _defineProperty(this, "editorOther", new InteractionGroup());
@@ -11,6 +11,10 @@
11
11
  * every repeat of a held key, and nothing cancels a key press. Pointing is counted on `pointerup`,
12
12
  * since a press cannot repeat but can be taken over by a scroll — which the browser does not count
13
13
  * either, and in that case `pointerup` never arrives.
14
+ *
15
+ * Counting events is also why an editor group's `totalCount` is not comparable with `page`'s, which
16
+ * is told `performance.interactionCount`: when an event is counted the collector cannot know whether
17
+ * the browser will open an interaction for it.
14
18
  */
15
19
  var INTERACTION_EVENTS = {
16
20
  keydown: {
@@ -4,7 +4,7 @@ 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 { bucketKeyForMs } from './bucket-boundaries';
7
+ import { bucketKeyForMs, REPORTING_THRESHOLD_MS } from './bucket-boundaries';
8
8
  /**
9
9
  * Latencies are counted per 8 ms, the resolution Event Timing reports durations at. Every
10
10
  * latency is rounded up to this step on the way in, which caps the number of distinct values
@@ -95,28 +95,73 @@ export var InteractionGroup = /*#__PURE__*/function () {
95
95
  value: function snapshot() {
96
96
  var _latencies;
97
97
  var totalCount = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.totalCount;
98
+ var observedCount = this.observedCount();
99
+ // `performance.interactionCount` can lag the entries the observer has delivered.
100
+ var reportedTotalCount = Math.max(totalCount, observedCount);
101
+ if (reportedTotalCount === 0) {
102
+ return {
103
+ totalCount: 0,
104
+ observedCount: 0,
105
+ sumMs: 0,
106
+ maxMs: 0,
107
+ buckets: {},
108
+ percentilesMs: {}
109
+ };
110
+ }
111
+
98
112
  // Ascending, so the reported buckets come out in order and the last latency is the
99
113
  // maximum. Sorted once for everything below.
100
114
  var latencies = Array.from(this.countByLatency.keys()).sort(function (a, b) {
101
115
  return a - b;
102
116
  });
103
- var observedCount = this.observedCount();
117
+
118
+ // A percentile is the interaction at position `ceil(quantile * total)` of all interactions
119
+ // sorted by latency. Event Timing never reports the ones under 16 ms, but they are the fastest,
120
+ // so they fill the front of the line, and the rank is the position among the measured ones:
121
+ //
122
+ // 250 interactions, 150 of them unmeasured
123
+ //
124
+ // position 1 ........... 150 | 151 ........... 245 .... 250
125
+ // latency unmeasured, < 16 ms | 16 ms ............. measured
126
+ // ^ p98
127
+ //
128
+ // p98 is position ceil(0.98 * 250) = 245 of all, rank 245 - 150 = 95 among the measured.
129
+ var unmeasuredCount = reportedTotalCount - observedCount;
104
130
  var percentileRanks = REPORTED_QUANTILES.map(function (quantile) {
105
131
  return {
106
132
  key: String(Math.round(quantile * 100)),
107
- rank: Math.max(1, Math.ceil(quantile * observedCount))
133
+ rank: Math.ceil(quantile * reportedTotalCount) - unmeasuredCount
108
134
  };
109
135
  });
110
136
  var buckets = {};
111
137
  var percentilesMs = {};
112
138
  var sumMs = 0;
113
139
  var counted = 0;
114
- var _iterator = _createForOfIteratorHelper(latencies),
140
+
141
+ // A rank of 0 or below is a latency Event Timing never reports, so the percentile gets the
142
+ // lowest one it does.
143
+ var _iterator = _createForOfIteratorHelper(percentileRanks),
115
144
  _step;
116
145
  try {
117
146
  for (_iterator.s(); !(_step = _iterator.n()).done;) {
147
+ var _step$value = _step.value,
148
+ key = _step$value.key,
149
+ rank = _step$value.rank;
150
+ if (rank <= 0) {
151
+ percentilesMs[key] = REPORTING_THRESHOLD_MS;
152
+ }
153
+ }
154
+ } catch (err) {
155
+ _iterator.e(err);
156
+ } finally {
157
+ _iterator.f();
158
+ }
159
+ var _iterator2 = _createForOfIteratorHelper(latencies),
160
+ _step2;
161
+ try {
162
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
118
163
  var _this$countByLatency$, _buckets$bucket;
119
- var latencyMs = _step.value;
164
+ var latencyMs = _step2.value;
120
165
  var count = (_this$countByLatency$ = this.countByLatency.get(latencyMs)) !== null && _this$countByLatency$ !== void 0 ? _this$countByLatency$ : 0;
121
166
  sumMs += latencyMs * count;
122
167
  var bucket = String(bucketKeyForMs(latencyMs));
@@ -125,31 +170,30 @@ export var InteractionGroup = /*#__PURE__*/function () {
125
170
  // A percentile is the latency the group's interactions reach counting up from the
126
171
  // fastest, so it is answered as soon as this many of them have been passed.
127
172
  counted += count;
128
- var _iterator2 = _createForOfIteratorHelper(percentileRanks),
129
- _step2;
173
+ var _iterator3 = _createForOfIteratorHelper(percentileRanks),
174
+ _step3;
130
175
  try {
131
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
132
- var _step2$value = _step2.value,
133
- key = _step2$value.key,
134
- rank = _step2$value.rank;
135
- if (percentilesMs[key] === undefined && counted >= rank) {
136
- percentilesMs[key] = latencyMs;
176
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
177
+ var _step3$value = _step3.value,
178
+ _key = _step3$value.key,
179
+ _rank = _step3$value.rank;
180
+ if (percentilesMs[_key] === undefined && counted >= _rank) {
181
+ percentilesMs[_key] = latencyMs;
137
182
  }
138
183
  }
139
184
  } catch (err) {
140
- _iterator2.e(err);
185
+ _iterator3.e(err);
141
186
  } finally {
142
- _iterator2.f();
187
+ _iterator3.f();
143
188
  }
144
189
  }
145
190
  } catch (err) {
146
- _iterator.e(err);
191
+ _iterator2.e(err);
147
192
  } finally {
148
- _iterator.f();
193
+ _iterator2.f();
149
194
  }
150
195
  return {
151
- // `performance.interactionCount` can lag the entries the observer has delivered.
152
- totalCount: Math.max(totalCount, observedCount),
196
+ totalCount: reportedTotalCount,
153
197
  observedCount: observedCount,
154
198
  sumMs: sumMs,
155
199
  maxMs: (_latencies = latencies[latencies.length - 1]) !== null && _latencies !== void 0 ? _latencies : 0,
@@ -161,17 +205,17 @@ export var InteractionGroup = /*#__PURE__*/function () {
161
205
  key: "observedCount",
162
206
  value: function observedCount() {
163
207
  var total = 0;
164
- var _iterator3 = _createForOfIteratorHelper(this.countByLatency.values()),
165
- _step3;
208
+ var _iterator4 = _createForOfIteratorHelper(this.countByLatency.values()),
209
+ _step4;
166
210
  try {
167
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
168
- var count = _step3.value;
211
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
212
+ var count = _step4.value;
169
213
  total += count;
170
214
  }
171
215
  } catch (err) {
172
- _iterator3.e(err);
216
+ _iterator4.e(err);
173
217
  } finally {
174
- _iterator3.f();
218
+ _iterator4.f();
175
219
  }
176
220
  return total;
177
221
  }
@@ -18,8 +18,8 @@ import { SnapshotScheduler } from './snapshot-scheduler';
18
18
  /**
19
19
  * Collects interaction latencies for one editor mount and emits session-to-date snapshots.
20
20
  *
21
- * Every interaction is counted in `page`, and the ones with the editor again in one of the editor
22
- * groups, which is what tells a slow editor apart from a slow page around it.
21
+ * Every interaction is counted in `page`, and the ones with the editor again in `editor` and in one
22
+ * of the editor groups, which is what tells a slow editor apart from a slow page around it.
23
23
  *
24
24
  * `start` and `stop` bound the collecting, which happens once. Within it there can be several
25
25
  * sessions, because a session covers one document in one mode: a Confluence live page
@@ -249,11 +249,16 @@ export var InteractivityCollector = /*#__PURE__*/function () {
249
249
  if (this.stopped) {
250
250
  return;
251
251
  }
252
+
253
+ // An event a script dispatched is no interaction: the browser counts none of them either.
254
+ if (!event.isTrusted) {
255
+ return;
256
+ }
252
257
  var group = this.session.tracker.recordEditorEvent(event);
253
258
  if (!group) {
254
259
  return;
255
260
  }
256
-
261
+ this.session.editor.countTotal();
257
262
  // The session names its editor groups after the fields they are reported in.
258
263
  this.session[group].countTotal();
259
264
  this.session.revision += 1;
@@ -287,6 +292,7 @@ export var InteractivityCollector = /*#__PURE__*/function () {
287
292
  var update = _step2.value;
288
293
  var groupsChanged = this.session.page.trackInteractionUpdate(update);
289
294
  if (update.group) {
295
+ groupsChanged = this.session.editor.trackInteractionUpdate(update) || groupsChanged;
290
296
  groupsChanged = this.session[update.group].trackInteractionUpdate(update) || groupsChanged;
291
297
  }
292
298
  var slowestChanged = (_this$session$slowest2 = this.session.slowest) === null || _this$session$slowest2 === void 0 ? void 0 : _this$session$slowest2.trackInteractionUpdate(entry, update);
@@ -353,6 +359,7 @@ export var InteractivityCollector = /*#__PURE__*/function () {
353
359
  editorDomSize: this.getEditorDomSize(),
354
360
  // Only `page` is told its total; each editor group has counted its own.
355
361
  page: session.page.snapshot(pageTotalCount),
362
+ editor: session.editor.snapshot(),
356
363
  editorTyping: session.editorTyping.snapshot(),
357
364
  editorPointer: session.editorPointer.snapshot(),
358
365
  editorOther: session.editorOther.snapshot()
@@ -29,6 +29,7 @@ export var InteractivitySession = /*#__PURE__*/_createClass(function Interactivi
29
29
  /** Page interaction count when the session opened, subtracted to get its own total. */
30
30
  _defineProperty(this, "interactionCountAtStart", InteractionObserver.readPageInteractionCount());
31
31
  _defineProperty(this, "page", new InteractionGroup());
32
+ _defineProperty(this, "editor", new InteractionGroup());
32
33
  _defineProperty(this, "editorTyping", new InteractionGroup());
33
34
  _defineProperty(this, "editorPointer", new InteractionGroup());
34
35
  _defineProperty(this, "editorOther", new InteractionGroup());
@@ -87,19 +87,16 @@ export type SlowInteraction = {
87
87
  * Session-to-date latency distribution for one group of interactions.
88
88
  *
89
89
  * `totalCount` counts every interaction, including those below the Event Timing reporting
90
- * threshold, so `totalCount - observedCount` is the sub-threshold count. `buckets` is keyed by
91
- * each bucket's upper boundary in milliseconds and is not cumulative; empty buckets are
90
+ * threshold, so `totalCount - observedCount` is the sub-threshold count. `percentilesMs` ranks over
91
+ * `totalCount`, so a percentile among the sub-threshold interactions reads as the threshold. `buckets`
92
+ * is keyed by each bucket's upper boundary in milliseconds and is not cumulative; empty buckets are
92
93
  * omitted, so a missing bucket means zero.
93
94
  */
94
95
  export type InteractionGroupSnapshot = {
95
96
  buckets: Record<string, number>;
96
97
  maxMs: number;
97
98
  observedCount: number;
98
- /**
99
- * Temporary. Percentiles of the same interactions, keyed by percentile and exact to the 8 ms
100
- * Event Timing reports durations at, to confirm that a percentile read off `buckets` lands
101
- * where the latencies actually are. Goes once that is established.
102
- */
99
+ /** Percentiles of the same interactions, keyed by percentile and exact to the 8 ms of Event Timing. */
103
100
  percentilesMs: Record<string, number>;
104
101
  sumMs: number;
105
102
  totalCount: number;
@@ -110,6 +107,8 @@ export type InteractionGroupSnapshot = {
110
107
  */
111
108
  export type InteractivitySnapshot = {
112
109
  activeMs: number;
110
+ /** The three editor groups as one; a percentile of the union cannot be derived from theirs. */
111
+ editor: InteractionGroupSnapshot;
113
112
  editorDomSize?: number;
114
113
  /** Interactions inside the editor that are neither typing nor pointing. */
115
114
  editorOther: InteractionGroupSnapshot;
@@ -11,8 +11,8 @@ 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.
14
+ * Every interaction is counted in `page`, and the ones with the editor again in `editor` and in one
15
+ * of the editor groups, which is what tells a slow editor apart from a slow page around it.
16
16
  *
17
17
  * `start` and `stop` bound the collecting, which happens once. Within it there can be several
18
18
  * sessions, because a session covers one document in one mode: a Confluence live page
@@ -21,6 +21,7 @@ export declare class InteractivitySession {
21
21
  readonly interactionCountAtStart: number;
22
22
  readonly tracker: InteractionTracker;
23
23
  readonly page: InteractionGroup;
24
+ readonly editor: InteractionGroup;
24
25
  readonly editorTyping: InteractionGroup;
25
26
  readonly editorPointer: InteractionGroup;
26
27
  readonly editorOther: InteractionGroup;
package/docs/0-intro.tsx CHANGED
@@ -24,8 +24,9 @@ ${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 — for the page as a whole, and for the editor's typing,
28
- pointer and other interactions — along with the slowest interactions of the session, per
27
+ histograms for full page editor sessions — for the page as a whole, for the editor as a whole, and
28
+ for the editor's typing, pointer and other interactions — along with the slowest interactions of
29
+ the session, per
29
30
  [RFC 095](https://hello.atlassian.net/wiki/spaces/EDITOR/pages/7527607488/Editor+RFC+095+Confluence+editor+responsiveness+bucketed+INP+telemetry).
30
31
  See the package README for the event shape, the snapshot cadence and what ends a session.
31
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-interactivity",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Interactivity plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -28,47 +28,11 @@
28
28
  "bind-event-listener": "^3.0.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@atlaskit/editor-common": "^120.10.0",
31
+ "@atlaskit/editor-common": "^120.16.0",
32
32
  "react": "^18.2.0 || ^19.2.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "react": "^19.2.0",
36
36
  "typescript": "npm:@typescript/typescript6@^6.0.2"
37
- },
38
- "techstack": {
39
- "@atlassian/frontend": {
40
- "code-structure": [
41
- "editor-plugin"
42
- ],
43
- "import-structure": [
44
- "atlassian-conventions"
45
- ],
46
- "circular-dependencies": [
47
- "file-and-folder-level"
48
- ]
49
- },
50
- "@repo/internal": {
51
- "dom-events": "use-bind-event-listener",
52
- "analytics": [
53
- "analytics-next"
54
- ],
55
- "design-tokens": [
56
- "color"
57
- ],
58
- "theming": [
59
- "react-context"
60
- ],
61
- "ui-components": [
62
- "lite-mode"
63
- ],
64
- "deprecation": "no-deprecated-imports",
65
- "styling": [
66
- "emotion",
67
- "emotion"
68
- ],
69
- "imports": [
70
- "import-no-extraneous-disable-for-examples-and-docs"
71
- ]
72
- }
73
37
  }
74
38
  }