@atlaskit/editor-plugin-interactivity 1.0.0 → 1.2.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 (43) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +79 -0
  3. package/afm-cc/tsconfig.json +3 -0
  4. package/afm-products/tsconfig.json +3 -0
  5. package/dist/cjs/collections/bounded-list.js +42 -0
  6. package/dist/cjs/collections/bounded-map.js +42 -0
  7. package/dist/cjs/collector/interaction-events.js +0 -7
  8. package/dist/cjs/collector/interaction-group.js +31 -4
  9. package/dist/cjs/collector/interaction-tracker.js +210 -68
  10. package/dist/cjs/collector/interactivity-collector.js +52 -22
  11. package/dist/cjs/collector/interactivity-session.js +3 -0
  12. package/dist/cjs/collector/long-animation-frame-observer.js +62 -0
  13. package/dist/cjs/collector/slow-interaction-list.js +413 -0
  14. package/dist/es2019/collections/bounded-list.js +24 -0
  15. package/dist/es2019/collections/bounded-map.js +25 -0
  16. package/dist/es2019/collector/interaction-events.js +0 -7
  17. package/dist/es2019/collector/interaction-group.js +28 -5
  18. package/dist/es2019/collector/interaction-tracker.js +201 -55
  19. package/dist/es2019/collector/interactivity-collector.js +35 -20
  20. package/dist/es2019/collector/interactivity-session.js +3 -0
  21. package/dist/es2019/collector/long-animation-frame-observer.js +42 -0
  22. package/dist/es2019/collector/slow-interaction-list.js +342 -0
  23. package/dist/esm/collections/bounded-list.js +35 -0
  24. package/dist/esm/collections/bounded-map.js +35 -0
  25. package/dist/esm/collector/interaction-events.js +0 -7
  26. package/dist/esm/collector/interaction-group.js +31 -5
  27. package/dist/esm/collector/interaction-tracker.js +210 -68
  28. package/dist/esm/collector/interactivity-collector.js +52 -22
  29. package/dist/esm/collector/interactivity-session.js +3 -0
  30. package/dist/esm/collector/long-animation-frame-observer.js +55 -0
  31. package/dist/esm/collector/slow-interaction-list.js +407 -0
  32. package/dist/types/analytics/interactivity-snapshot.d.ts +65 -0
  33. package/dist/types/collections/bounded-list.d.ts +9 -0
  34. package/dist/types/collections/bounded-map.d.ts +9 -0
  35. package/dist/types/collector/interaction-events.d.ts +1 -7
  36. package/dist/types/collector/interaction-group.d.ts +18 -5
  37. package/dist/types/collector/interaction-tracker.d.ts +67 -13
  38. package/dist/types/collector/interactivity-collector.d.ts +3 -0
  39. package/dist/types/collector/interactivity-session.d.ts +2 -0
  40. package/dist/types/collector/long-animation-frame-observer.d.ts +28 -0
  41. package/dist/types/collector/slow-interaction-list.d.ts +64 -0
  42. package/docs/0-intro.tsx +2 -1
  43. package/package.json +4 -3
@@ -1,21 +1,54 @@
1
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
1
2
  import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
2
3
  import _createClass from "@babel/runtime/helpers/createClass";
3
4
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
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
- 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
- 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; }
5
+ import { BoundedList } from '../collections/bounded-list';
6
+ import { BoundedMap } from '../collections/bounded-map';
7
7
  import { interactionEventKind } from './interaction-events';
8
8
 
9
9
  /**
10
- * The Event Timing fields this package reads. `interactionId` is missing from the DOM
11
- * typings' `PerformanceEventTiming`, so it is declared here as optional, which also makes
12
- * a `PerformanceEntry` from `getEntries()` assignable without a cast.
10
+ * The Event Timing fields this package reads, declared optional so that a `PerformanceEntry` from
11
+ * `getEntries()` is assignable without a cast `interactionId` is missing from the DOM typings'
12
+ * `PerformanceEventTiming` altogether, and the rest are only on it.
13
+ */
14
+
15
+ /**
16
+ * One paint, and the processing of every event it presented.
17
+ *
18
+ * One paint can present several events, and the handlers of all of them ran before it: a
19
+ * `pointerover` handler that was still running when the user clicked held up the paint that showed
20
+ * the click. So an interaction's processing is the processing of its whole paint, not of its own
21
+ * events only — otherwise a handler that is not its own reads as time the user waited for nothing.
22
+ *
23
+ * Event Timing gives a paint no identity. The only thing an entry says about it is
24
+ * `startTime + duration`, the moment it happened, so that is what identifies it.
25
+ */
26
+
27
+ /**
28
+ * The paint an entry was presented by, and whether the entry moved the processing that paint covers.
29
+ * An entry whose handlers ran inside what the paint already covered changes nothing for any
30
+ * interaction reading its boundaries from it.
31
+ */
32
+
33
+ /**
34
+ * The four moments an interaction's latency divides at, in order: the user acted, its handlers
35
+ * started running, they finished, the screen updated.
13
36
  */
14
37
 
15
38
  /**
16
39
  * What an entry did to the interaction it belongs to: either it is the first entry of a new
17
- * interaction, or it measured an interaction that was already counted as slower than it was
18
- * known to be. Both carry the editor group of the interaction, if it is one of the editor's.
40
+ * interaction, or it changed an interaction already known. Both carry the editor group of the
41
+ * interaction, if it is one of the editor's, and the boundaries it now has.
42
+ *
43
+ * A `remeasured` where `previousLatencyMs` equals `latencyMs` is an interaction whose latency stayed
44
+ * as it was and whose boundaries moved: the entry ran in the same paint without being the slowest
45
+ * of them.
46
+ */
47
+
48
+ /**
49
+ * What is kept per interaction. The boundaries are not among these: they are derived from the paint
50
+ * whenever the interaction is reported, because the paint keeps growing as the browser reports the
51
+ * remaining events it presented.
19
52
  */
20
53
 
21
54
  /**
@@ -24,11 +57,17 @@ import { interactionEventKind } from './interaction-events';
24
57
  * anything older than the last few hundred is not needed.
25
58
  */
26
59
  var MAX_TRACKED = 256;
60
+
61
+ /**
62
+ * How many paints entries can still be placed in. The entries of a paint arrive within a batch or
63
+ * two of each other, so this only has to cover the paints in flight; it is what `web-vitals` keeps.
64
+ */
65
+ var MAX_RECENT_PAINTS = 10;
27
66
  /**
28
- * How many are dropped per cleanup. Dropping one at a time would run a cleanup on every new
29
- * interaction once the limit is reached; a batch makes it one cleanup per 64 of them.
67
+ * Event Timing rounds `duration` down to 8 ms, so two events presented by one paint report that
68
+ * paint up to this far apart and nothing else in Event Timing says they share it.
30
69
  */
31
- var PRUNE_BATCH = 64;
70
+ var PRESENTATION_ROUNDING_MS = 8;
32
71
 
33
72
  /**
34
73
  * Identifies the event an entry measured: an entry's `startTime` is that event's timestamp and its
@@ -54,6 +93,9 @@ function eventKey(type, timeStamp) {
54
93
  *
55
94
  * The editor's events answer what an entry cannot: which interactions were with the editor, and
56
95
  * how many there were, including the ones below the Event Timing reporting threshold.
96
+ *
97
+ * Every entry is also placed in the paint that presented it, which is what says how an
98
+ * interaction's latency divides into waiting, processing and presentation. See `Paint`.
57
99
  */
58
100
  export var InteractionTracker = /*#__PURE__*/function () {
59
101
  /**
@@ -66,8 +108,9 @@ export var InteractionTracker = /*#__PURE__*/function () {
66
108
  function InteractionTracker() {
67
109
  var startsAfterInteractionId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
68
110
  _classCallCheck(this, InteractionTracker);
69
- _defineProperty(this, "interactions", new Map());
70
- _defineProperty(this, "groupByEvent", new Map());
111
+ _defineProperty(this, "interactions", new BoundedMap(MAX_TRACKED));
112
+ _defineProperty(this, "groupByEvent", new BoundedMap(MAX_TRACKED));
113
+ _defineProperty(this, "recentPaints", new BoundedList(MAX_RECENT_PAINTS));
71
114
  _defineProperty(this, "highestInteractionId", 0);
72
115
  this.startsAfterInteractionId = startsAfterInteractionId;
73
116
  }
@@ -82,58 +125,67 @@ export var InteractionTracker = /*#__PURE__*/function () {
82
125
  /**
83
126
  * Merges an entry into the interaction it belongs to.
84
127
  *
85
- * @returns what that did to the interaction's latency, or nothing when the entry is not
86
- * part of an interaction, belongs to a previous tracker, or does not change one.
128
+ * @returns what that changed about the interactions this tracker knows, the entry's own first.
129
+ * More than one of them when the paint the entry ran in presented several.
87
130
  */
88
131
  }, {
89
132
  key: "merge",
90
133
  value: function merge(entry) {
134
+ if (!Number.isFinite(entry.duration) || entry.duration < 0) {
135
+ return [];
136
+ }
137
+ var placement = this.paintOf(entry);
138
+ var paint = placement === null || placement === void 0 ? void 0 : placement.paint;
91
139
  var interactionId = entry.interactionId;
92
- // `first-input` and non-interaction events report `interactionId` 0.
140
+
141
+ // Reported only when the entry grew the paint, because otherwise nothing an interaction reads
142
+ // from it moved. Every interaction the paint presented is here, not only the entry's own: a
143
+ // `first-input` or non-interaction event reports `interactionId` 0 and has none of its own,
144
+ // and a second press of the same paint moved where the first one spent its latency.
145
+ //
146
+ // The check below is neither reached with a `0` nor needed: the interactions reported are the
147
+ // ones this tracker holds, and the only way into that map is past the check.
148
+ var remeasuredOthers = placement !== null && placement !== void 0 && placement.grew ? this.remeasuredUpdatesIn(placement.paint, {
149
+ except: interactionId
150
+ }) : [];
93
151
  if (!interactionId) {
94
- return undefined;
152
+ return remeasuredOthers;
95
153
  }
96
154
  if (interactionId <= this.startsAfterInteractionId) {
97
- return undefined;
155
+ // The entry belongs to the session before this one, but its handlers still ran before a
156
+ // paint of this one.
157
+ return remeasuredOthers;
98
158
  }
99
159
  this.highestInteractionId = Math.max(this.highestInteractionId, interactionId);
100
-
101
- // Every latency is counted through here, so this is where one that cannot be measured
102
- // is rejected: a `NaN` getting through becomes a `NaN` bucket key and a `NaN` `sumMs`
103
- // for the rest of the session.
104
- if (!Number.isFinite(entry.duration) || entry.duration < 0) {
105
- return undefined;
106
- }
107
160
  var tracked = this.interactions.get(interactionId);
108
161
  if (tracked === undefined) {
109
162
  // Taken once: an entry that only makes the interaction slower has to move its count
110
163
  // within the group it was counted in, not into another one.
111
164
  var group = this.groupByEvent.get(eventKey(entry.name, entry.startTime));
112
- this.interactions.set(interactionId, {
165
+ var interaction = {
166
+ group: group,
113
167
  latencyMs: entry.duration,
114
- group: group
115
- });
116
- this.prune(this.interactions);
117
- return {
118
- type: 'new',
119
- latencyMs: entry.duration,
120
- group: group
168
+ presentedIn: paint,
169
+ startedAt: entry.startTime
121
170
  };
171
+ this.interactions.set(interactionId, interaction);
172
+ return [this.newUpdate(interactionId, interaction)].concat(_toConsumableArray(remeasuredOthers));
122
173
  }
123
- if (entry.duration <= tracked.latencyMs) {
124
- return undefined;
174
+ if (entry.duration > tracked.latencyMs) {
175
+ var previousLatencyMs = tracked.latencyMs;
176
+ tracked.latencyMs = entry.duration;
177
+ tracked.presentedIn = paint;
178
+ tracked.startedAt = entry.startTime;
179
+ return [this.remeasuredUpdate(interactionId, tracked, previousLatencyMs)].concat(_toConsumableArray(remeasuredOthers));
125
180
  }
126
- var fromMs = tracked.latencyMs;
127
- tracked.latencyMs = entry.duration;
128
- return {
129
- type: 'remeasured',
130
- fromMs: fromMs,
131
- toMs: entry.duration,
132
- group: tracked.group
133
- };
134
- }
135
181
 
136
- /** @returns the group of an interaction to count, when this is the event its group counts on. */
182
+ // Not the slowest entry of the interaction, so its latency stands. Its handlers still ran
183
+ // before the same paint, if this is that paint, and so moved where that latency went.
184
+ if (!(placement !== null && placement !== void 0 && placement.grew) || paint !== tracked.presentedIn) {
185
+ return remeasuredOthers;
186
+ }
187
+ return [this.remeasuredUpdate(interactionId, tracked, tracked.latencyMs)].concat(_toConsumableArray(remeasuredOthers));
188
+ }
137
189
  }, {
138
190
  key: "recordEditorEvent",
139
191
  value: function recordEditorEvent(event) {
@@ -145,35 +197,125 @@ export var InteractionTracker = /*#__PURE__*/function () {
145
197
  // Every event of the interaction, because any of them can be the one Event Timing reports
146
198
  // as the slowest: for a pointer press that is usually the click.
147
199
  this.groupByEvent.set(eventKey(event.type, event.timeStamp), kind.group);
148
- this.prune(this.groupByEvent);
149
200
  return kind.counts ? kind.group : undefined;
150
201
  }
202
+
203
+ /**
204
+ * Every interaction whose boundaries are read from this paint, reported as measured again at the
205
+ * latency it already had.
206
+ *
207
+ * @param except the interaction the entry measured, which the caller reports itself. `0` or
208
+ * nothing when the entry measured none, and then no interaction is left out.
209
+ */
210
+ }, {
211
+ key: "remeasuredUpdatesIn",
212
+ value: function remeasuredUpdatesIn(paint, _ref) {
213
+ var _this = this;
214
+ var except = _ref.except;
215
+ var remeasured = [];
216
+ this.interactions.forEach(function (interaction, interactionId) {
217
+ if (interaction.presentedIn === paint && interactionId !== except) {
218
+ remeasured.push(_this.remeasuredUpdate(interactionId, interaction, interaction.latencyMs));
219
+ }
220
+ });
221
+ return remeasured;
222
+ }
223
+ }, {
224
+ key: "newUpdate",
225
+ value: function newUpdate(interactionId, tracked) {
226
+ return {
227
+ type: 'new',
228
+ interactionId: interactionId,
229
+ latencyMs: tracked.latencyMs,
230
+ group: tracked.group,
231
+ boundaries: this.boundariesOf(tracked)
232
+ };
233
+ }
151
234
  }, {
152
- key: "prune",
153
- value: function prune(entries) {
154
- if (entries.size <= MAX_TRACKED) {
155
- return;
235
+ key: "remeasuredUpdate",
236
+ value: function remeasuredUpdate(interactionId, tracked, previousLatencyMs) {
237
+ return {
238
+ type: 'remeasured',
239
+ interactionId: interactionId,
240
+ previousLatencyMs: previousLatencyMs,
241
+ latencyMs: tracked.latencyMs,
242
+ group: tracked.group,
243
+ boundaries: this.boundariesOf(tracked)
244
+ };
245
+ }
246
+
247
+ /**
248
+ * The four moments of an interaction, read from the paint as it stands now.
249
+ *
250
+ * Limited the way `web-vitals` limits its INP attribution, so the four stay in order: the paint's
251
+ * handlers can have started before the event arrived, and can have finished after the paint the
252
+ * event's rounded-down `duration` points at.
253
+ *
254
+ * @returns nothing when the browser reported no processing timestamps for the interaction, which
255
+ * leaves it in no paint.
256
+ */
257
+ }, {
258
+ key: "boundariesOf",
259
+ value: function boundariesOf(_ref2) {
260
+ var latencyMs = _ref2.latencyMs,
261
+ presentedIn = _ref2.presentedIn,
262
+ startedAt = _ref2.startedAt;
263
+ if (!presentedIn) {
264
+ return undefined;
156
265
  }
266
+ var processingStartedAt = Math.max(presentedIn.processingStartedAt, startedAt);
267
+ var presentedAt = Math.max(startedAt + latencyMs, processingStartedAt);
268
+ var processingEndedAt = Math.min(presentedIn.processingEndedAt, presentedAt);
269
+ return {
270
+ startedAt: startedAt,
271
+ processingStartedAt: processingStartedAt,
272
+ processingEndedAt: processingEndedAt,
273
+ presentedAt: presentedAt
274
+ };
275
+ }
157
276
 
158
- // `Map` preserves insertion order, so the entries inserted first are the least likely to
159
- // see another entry or another event.
160
- var remaining = PRUNE_BATCH;
161
- var _iterator = _createForOfIteratorHelper(entries.keys()),
162
- _step;
163
- try {
164
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
165
- var key = _step.value;
166
- entries.delete(key);
167
- remaining -= 1;
168
- if (remaining === 0) {
169
- return;
170
- }
171
- }
172
- } catch (err) {
173
- _iterator.e(err);
174
- } finally {
175
- _iterator.f();
277
+ /**
278
+ * The paint that presented this entry, grown to cover this entry's own processing.
279
+ *
280
+ * The moment being matched is always the one the first entry of the paint reported, so that a
281
+ * run of entries 8 ms apart cannot walk one paint across the next.
282
+ *
283
+ * @returns nothing when the browser reported no processing timestamps for the entry, which
284
+ * leaves nothing to place it by.
285
+ */
286
+ }, {
287
+ key: "paintOf",
288
+ value: function paintOf(entry) {
289
+ var startTime = entry.startTime,
290
+ duration = entry.duration,
291
+ processingStart = entry.processingStart,
292
+ processingEnd = entry.processingEnd;
293
+ if (typeof processingStart !== 'number' || typeof processingEnd !== 'number') {
294
+ return undefined;
176
295
  }
296
+ var presentedAt = startTime + duration;
297
+ var knownPaint = this.recentPaints.findLast(function (paint) {
298
+ return Math.abs(presentedAt - paint.presentedAt) <= PRESENTATION_ROUNDING_MS;
299
+ });
300
+ if (knownPaint) {
301
+ var grew = processingStart < knownPaint.processingStartedAt || processingEnd > knownPaint.processingEndedAt;
302
+ knownPaint.processingStartedAt = Math.min(processingStart, knownPaint.processingStartedAt);
303
+ knownPaint.processingEndedAt = Math.max(processingEnd, knownPaint.processingEndedAt);
304
+ return {
305
+ grew: grew,
306
+ paint: knownPaint
307
+ };
308
+ }
309
+ var newPaint = {
310
+ presentedAt: presentedAt,
311
+ processingStartedAt: processingStart,
312
+ processingEndedAt: processingEnd
313
+ };
314
+ this.recentPaints.push(newPaint);
315
+ return {
316
+ grew: true,
317
+ paint: newPaint
318
+ };
177
319
  }
178
320
  }]);
179
321
  }();
@@ -1,15 +1,19 @@
1
1
  import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
2
2
  import _createClass from "@babel/runtime/helpers/createClass";
3
3
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
5
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
6
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
5
7
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
6
8
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
7
9
  import { getDocument } from '@atlaskit/browser-apis';
10
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
8
11
  import { EditorEventObserver } from './editor-event-observer';
9
12
  import { InteractionObserver } from './interaction-observer';
10
13
  import { InteractivitySession } from './interactivity-session';
11
14
  import { SCHEMA_VERSION } from './bucket-boundaries';
12
15
  import { LifecycleObserver } from './lifecycle-observer';
16
+ import { LongAnimationFrameObserver } from './long-animation-frame-observer';
13
17
  import { SnapshotScheduler } from './snapshot-scheduler';
14
18
  /**
15
19
  * Collects interaction latencies for one editor mount and emits session-to-date snapshots.
@@ -36,6 +40,9 @@ export var InteractivityCollector = /*#__PURE__*/function () {
36
40
  this.interactionObserver = new InteractionObserver(function (entries) {
37
41
  return _this.recordEntries(entries);
38
42
  });
43
+ this.longAnimationFrameObserver = isExperimentEnabled('platform_editor_editor_interactivity_slowest') ? new LongAnimationFrameObserver(function (frames) {
44
+ return _this.recordFrames(frames);
45
+ }) : undefined;
39
46
  this.editorEvents = new EditorEventObserver(function (event) {
40
47
  return _this.recordEditorEvent(event);
41
48
  });
@@ -68,6 +75,7 @@ export var InteractivityCollector = /*#__PURE__*/function () {
68
75
  return _createClass(InteractivityCollector, [{
69
76
  key: "start",
70
77
  value: function start() {
78
+ var _this$longAnimationFr;
71
79
  // Collecting is over for good once stopped: the session it covered has been reported.
72
80
  if (this.stopped) {
73
81
  return false;
@@ -83,6 +91,7 @@ export var InteractivityCollector = /*#__PURE__*/function () {
83
91
  // this object was constructed.
84
92
  this.session = this.createSession(0);
85
93
  this.interactionObserver.start();
94
+ (_this$longAnimationFr = this.longAnimationFrameObserver) === null || _this$longAnimationFr === void 0 || _this$longAnimationFr.start();
86
95
  this.editorEvents.observe(this.editorRoot);
87
96
  this.snapshotScheduler.start();
88
97
  this.lifecycleObserver.start();
@@ -94,12 +103,14 @@ export var InteractivityCollector = /*#__PURE__*/function () {
94
103
  }, {
95
104
  key: "stop",
96
105
  value: function stop() {
106
+ var _this$longAnimationFr2;
97
107
  if (this.stopped) {
98
108
  return;
99
109
  }
100
110
  this.takeSnapshot('unmount');
101
111
  this.stopped = true;
102
112
  this.interactionObserver.stop();
113
+ (_this$longAnimationFr2 = this.longAnimationFrameObserver) === null || _this$longAnimationFr2 === void 0 || _this$longAnimationFr2.stop();
103
114
  this.editorEvents.stop();
104
115
  this.snapshotScheduler.stop();
105
116
  this.lifecycleObserver.stop();
@@ -247,33 +258,47 @@ export var InteractivityCollector = /*#__PURE__*/function () {
247
258
  this.session[group].countTotal();
248
259
  this.session.revision += 1;
249
260
  }
261
+
262
+ /** Long Animation Frames say where the latency of a slow interaction went. */
263
+ }, {
264
+ key: "recordFrames",
265
+ value: function recordFrames(frames) {
266
+ var _this$session$slowest;
267
+ if (this.stopped) {
268
+ return;
269
+ }
270
+ if ((_this$session$slowest = this.session.slowest) !== null && _this$session$slowest !== void 0 && _this$session$slowest.trackLongAnimationFrames(frames)) {
271
+ this.session.revision += 1;
272
+ }
273
+ }
250
274
  }, {
251
275
  key: "recordEntries",
252
276
  value: function recordEntries(entries) {
253
- var _this$session = this.session,
254
- tracker = _this$session.tracker,
255
- page = _this$session.page;
256
277
  var _iterator = _createForOfIteratorHelper(entries),
257
278
  _step;
258
279
  try {
259
280
  for (_iterator.s(); !(_step = _iterator.n()).done;) {
260
281
  var entry = _step.value;
261
- var update = tracker.merge(entry);
262
- if (!update) {
263
- continue;
264
- }
265
- if (update.type === 'new') {
266
- page.add(update.latencyMs);
267
- if (update.group) {
268
- this.session[update.group].add(update.latencyMs);
269
- }
270
- } else {
271
- page.remeasure(update.fromMs, update.toMs);
272
- if (update.group) {
273
- this.session[update.group].remeasure(update.fromMs, update.toMs);
282
+ var _iterator2 = _createForOfIteratorHelper(this.session.tracker.merge(entry)),
283
+ _step2;
284
+ try {
285
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
286
+ var _this$session$slowest2;
287
+ var update = _step2.value;
288
+ var groupsChanged = this.session.page.trackInteractionUpdate(update);
289
+ if (update.group) {
290
+ groupsChanged = this.session[update.group].trackInteractionUpdate(update) || groupsChanged;
291
+ }
292
+ var slowestChanged = (_this$session$slowest2 = this.session.slowest) === null || _this$session$slowest2 === void 0 ? void 0 : _this$session$slowest2.trackInteractionUpdate(entry, update);
293
+ if (groupsChanged || slowestChanged) {
294
+ this.session.revision += 1;
295
+ }
274
296
  }
297
+ } catch (err) {
298
+ _iterator2.e(err);
299
+ } finally {
300
+ _iterator2.f();
275
301
  }
276
- this.session.revision += 1;
277
302
  }
278
303
  } catch (err) {
279
304
  _iterator.e(err);
@@ -284,6 +309,7 @@ export var InteractivityCollector = /*#__PURE__*/function () {
284
309
  }, {
285
310
  key: "takeSnapshot",
286
311
  value: function takeSnapshot(reason) {
312
+ var _this$longAnimationFr3, _session$slowest;
287
313
  if (this.stopped) {
288
314
  return;
289
315
  }
@@ -297,10 +323,11 @@ export var InteractivityCollector = /*#__PURE__*/function () {
297
323
  return;
298
324
  }
299
325
 
300
- // Must run before the change check: the browser may be holding entries that have not
301
- // reached the observer callback yet, and on `pagehide` there is no later chance to
302
- // pick them up.
326
+ // Must run before the change check: the browser may be holding entries and frames that have
327
+ // not reached the observer callbacks yet, and on `pagehide` there is no later chance to
328
+ // pick them up. Entries first, so a record the frames answer for exists by then.
303
329
  this.interactionObserver.drain();
330
+ (_this$longAnimationFr3 = this.longAnimationFrameObserver) === null || _this$longAnimationFr3 === void 0 || _this$longAnimationFr3.drain();
304
331
  if (session.revision === session.emittedRevision) {
305
332
  return;
306
333
  }
@@ -312,7 +339,8 @@ export var InteractivityCollector = /*#__PURE__*/function () {
312
339
  var now = performance.now();
313
340
  var hiddenMs = session.hiddenMs + (session.hiddenSince === undefined ? 0 : now - session.hiddenSince);
314
341
  var pageTotalCount = InteractionObserver.readPageInteractionCount() - session.interactionCountAtStart;
315
- this.emit({
342
+ var slowest = (_session$slowest = session.slowest) === null || _session$slowest === void 0 ? void 0 : _session$slowest.snapshot();
343
+ this.emit(_objectSpread({
316
344
  schema: SCHEMA_VERSION,
317
345
  interactivitySessionId: session.id,
318
346
  objectId: session.objectId,
@@ -328,7 +356,9 @@ export var InteractivityCollector = /*#__PURE__*/function () {
328
356
  editorTyping: session.editorTyping.snapshot(),
329
357
  editorPointer: session.editorPointer.snapshot(),
330
358
  editorOther: session.editorOther.snapshot()
331
- });
359
+ }, slowest && {
360
+ slowest: slowest
361
+ }));
332
362
  }
333
363
  }]);
334
364
  }();
@@ -1,9 +1,11 @@
1
1
  import _createClass from "@babel/runtime/helpers/createClass";
2
2
  import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
3
3
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
+ import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
4
5
  import { InteractionObserver } from './interaction-observer';
5
6
  import { InteractionTracker } from './interaction-tracker';
6
7
  import { InteractionGroup } from './interaction-group';
8
+ import { SlowInteractionList } from './slow-interaction-list';
7
9
  function createSessionId() {
8
10
  if (typeof crypto.randomUUID === 'function') {
9
11
  return crypto.randomUUID();
@@ -30,6 +32,7 @@ export var InteractivitySession = /*#__PURE__*/_createClass(function Interactivi
30
32
  _defineProperty(this, "editorTyping", new InteractionGroup());
31
33
  _defineProperty(this, "editorPointer", new InteractionGroup());
32
34
  _defineProperty(this, "editorOther", new InteractionGroup());
35
+ _defineProperty(this, "slowest", isExperimentEnabled('platform_editor_editor_interactivity_slowest') ? new SlowInteractionList() : undefined);
33
36
  /** Increments per snapshot; a query takes the highest one per session. */
34
37
  _defineProperty(this, "seq", 0);
35
38
  _defineProperty(this, "hiddenMs", 0);
@@ -0,0 +1,55 @@
1
+ import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
2
+ import _createClass from "@babel/runtime/helpers/createClass";
3
+ /**
4
+ * Reports the frames the browser took longer than 50 ms to render to `onFrames`. They are what says
5
+ * where the time of a slow interaction went — which script ran the longest while the user waited,
6
+ * and how much of the wait was style and layout — which Event Timing cannot answer.
7
+ *
8
+ * What they say about an interaction is `SlowInteractionList`'s to work out; this only observes.
9
+ */
10
+ export var LongAnimationFrameObserver = /*#__PURE__*/function () {
11
+ function LongAnimationFrameObserver(onFrames) {
12
+ _classCallCheck(this, LongAnimationFrameObserver);
13
+ this.onFrames = onFrames;
14
+ }
15
+ return _createClass(LongAnimationFrameObserver, [{
16
+ key: "start",
17
+ value: function start() {
18
+ var _this = this;
19
+ if (this.observer || !LongAnimationFrameObserver.isSupported()) {
20
+ return;
21
+ }
22
+ this.observer = new PerformanceObserver(function (list) {
23
+ _this.onFrames(list.getEntries());
24
+ });
25
+
26
+ // Buffered, as `web-vitals` observes them: a frame reported before the editor mounted can
27
+ // still be the one an interaction right after it ran in.
28
+ this.observer.observe({
29
+ type: 'long-animation-frame',
30
+ buffered: true
31
+ });
32
+ }
33
+ }, {
34
+ key: "drain",
35
+ value: function drain() {
36
+ var _this$observer;
37
+ var frames = (_this$observer = this.observer) === null || _this$observer === void 0 ? void 0 : _this$observer.takeRecords();
38
+ if (frames) {
39
+ this.onFrames(frames);
40
+ }
41
+ }
42
+ }, {
43
+ key: "stop",
44
+ value: function stop() {
45
+ var _this$observer2;
46
+ (_this$observer2 = this.observer) === null || _this$observer2 === void 0 || _this$observer2.disconnect();
47
+ this.observer = undefined;
48
+ }
49
+ }], [{
50
+ key: "isSupported",
51
+ value: function isSupported() {
52
+ return typeof PerformanceObserver !== 'undefined' && PerformanceObserver.supportedEntryTypes.includes('long-animation-frame');
53
+ }
54
+ }]);
55
+ }();