@atlaskit/editor-plugin-interactivity 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +46 -1
  3. package/dist/cjs/collections/bounded-list.js +42 -0
  4. package/dist/cjs/collections/bounded-map.js +42 -0
  5. package/dist/cjs/collector/interaction-events.js +4 -0
  6. package/dist/cjs/collector/interaction-group.js +31 -4
  7. package/dist/cjs/collector/interaction-tracker.js +207 -67
  8. package/dist/cjs/collector/interactivity-collector.js +50 -35
  9. package/dist/cjs/collector/long-animation-frame-observer.js +62 -0
  10. package/dist/cjs/collector/slow-interaction-list.js +297 -56
  11. package/dist/es2019/collections/bounded-list.js +24 -0
  12. package/dist/es2019/collections/bounded-map.js +25 -0
  13. package/dist/es2019/collector/interaction-events.js +4 -0
  14. package/dist/es2019/collector/interaction-group.js +28 -5
  15. package/dist/es2019/collector/interaction-tracker.js +198 -54
  16. package/dist/es2019/collector/interactivity-collector.js +35 -34
  17. package/dist/es2019/collector/long-animation-frame-observer.js +42 -0
  18. package/dist/es2019/collector/slow-interaction-list.js +251 -57
  19. package/dist/esm/collections/bounded-list.js +35 -0
  20. package/dist/esm/collections/bounded-map.js +35 -0
  21. package/dist/esm/collector/interaction-events.js +4 -0
  22. package/dist/esm/collector/interaction-group.js +31 -5
  23. package/dist/esm/collector/interaction-tracker.js +207 -67
  24. package/dist/esm/collector/interactivity-collector.js +50 -35
  25. package/dist/esm/collector/long-animation-frame-observer.js +55 -0
  26. package/dist/esm/collector/slow-interaction-list.js +297 -56
  27. package/dist/types/analytics/interactivity-snapshot.d.ts +32 -1
  28. package/dist/types/collections/bounded-list.d.ts +9 -0
  29. package/dist/types/collections/bounded-map.d.ts +9 -0
  30. package/dist/types/collector/interaction-group.d.ts +18 -5
  31. package/dist/types/collector/interaction-tracker.d.ts +58 -9
  32. package/dist/types/collector/interactivity-collector.d.ts +3 -0
  33. package/dist/types/collector/long-animation-frame-observer.d.ts +28 -0
  34. package/dist/types/collector/slow-interaction-list.d.ts +44 -18
  35. package/package.json +3 -39
@@ -1,4 +1,5 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import { BoundedList } from '../collections/bounded-list';
2
3
  /** Fixed by the schema, so the event stays a bounded size. */
3
4
  const MAX_RECORDS = 5;
4
5
 
@@ -8,6 +9,12 @@ const MAX_RECORDS = 5;
8
9
  */
9
10
  const MIN_LATENCY_MS = 200;
10
11
 
12
+ /**
13
+ * How many frames are kept to attribute records from. An interaction spans one paint, so a few dozen
14
+ * cover even a second of a janky page.
15
+ */
16
+ const MAX_FRAMES = 64;
17
+
11
18
  /**
12
19
  * The attributes a target may be named by, all of them ours: `data-vc` for visual completion, the
13
20
  * test ids for tests. Ids, roles, class names, text and accessibility labels are left out because
@@ -18,13 +25,40 @@ const MAX_TARGET_ELEMENTS = 4;
18
25
  const MAX_ATTRIBUTE_VALUE_LENGTH = 32;
19
26
  const MAX_TARGET_LENGTH = 120;
20
27
 
21
- /** The id identifies an interaction across the entries measuring it. It is not reported. */
28
+ /** How long a reported script or function name may be. */
29
+ const MAX_NAME_LENGTH = 64;
30
+ const QUERY_OR_HASH = /[?#]/u;
31
+
32
+ /** The part of a record that only the frames the interaction ran in can fill in. */
22
33
 
23
- /** An interaction as the collector's two observers saw it. */
34
+ /**
35
+ * Whether two attributions say the same thing. Shallow, because every field of one is a number or a
36
+ * string; by the field names of both, so that a field going missing counts as a change rather than
37
+ * as nothing to see.
38
+ */
39
+ function sameAttribution(one, other) {
40
+ const fields = Object.keys(one);
41
+ return fields.length === Object.keys(other).length && fields.every(field => one[field] === other[field]);
42
+ }
43
+
44
+ /**
45
+ * One recorded interaction. The reported phases are not among its fields: they are the gaps between
46
+ * the boundaries, worked out when the record is reported, which is also where the latency is
47
+ * rounded into the `durationMs` the event carries.
48
+ *
49
+ * The attribution is kept whole rather than spread across the record, so that a record can never
50
+ * hold half of what one set of frames said and half of what another did.
51
+ *
52
+ * `interactionId` identifies the interaction across the entries measuring it, and `boundaries` is
53
+ * also what its frames are matched to it by. Neither is reported.
54
+ */
24
55
 
25
56
  /**
26
57
  * The slowest interactions of one session, which is what the event's `slowest` records are.
27
58
  *
59
+ * Interactions arrive from the tracker and frames from the Long Animation Frame observer, and this
60
+ * is where the two meet: a record says both how long the user waited and where that time went.
61
+ *
28
62
  * A record is built from the entry that measured the interaction, as that entry arrives:
29
63
  * `entry.target` is `null` once the element has left the document.
30
64
  */
@@ -32,84 +66,244 @@ export class SlowInteractionList {
32
66
  constructor() {
33
67
  /** Slowest first. */
34
68
  _defineProperty(this, "records", []);
69
+ _defineProperty(this, "frames", new BoundedList(MAX_FRAMES));
35
70
  }
36
- track(interaction) {
71
+ /**
72
+ * Takes in what the tracker now says about an interaction, keeping it when it is one of the
73
+ * slowest of the session.
74
+ *
75
+ * @returns whether that changed what a snapshot would carry.
76
+ */
77
+ trackInteractionUpdate(entry, update) {
37
78
  const {
79
+ boundaries,
38
80
  interactionId,
39
81
  latencyMs
40
- } = interaction;
41
- const previousIndex = this.records.findIndex(existing => existing.interactionId === interactionId);
42
- let toBeatMs = MIN_LATENCY_MS;
43
- if (previousIndex !== -1) {
44
- // An interaction's latency grows as later entries arrive, and it replaces itself rather
45
- // than taking a second place.
46
- toBeatMs = this.records[previousIndex].durationMs;
47
- } else if (this.records.length === MAX_RECORDS) {
48
- toBeatMs = this.records[MAX_RECORDS - 1].durationMs;
49
- }
50
- if (latencyMs <= toBeatMs) {
51
- return;
82
+ } = update;
83
+ const index = this.records.findIndex(record => record.interactionId === interactionId);
84
+ const knownRecord = index === -1 ? undefined : this.records[index];
85
+ if (knownRecord && latencyMs <= knownRecord.latencyMs) {
86
+ var _knownRecord$boundari, _knownRecord$boundari2;
87
+ // The interaction at the latency it already had, so its name and target still come from
88
+ // the entry that measured it at its slowest — and so do `startedAt` and `presentedAt`,
89
+ // which leaves the processing as the only pair that can have moved.
90
+ if (((_knownRecord$boundari = knownRecord.boundaries) === null || _knownRecord$boundari === void 0 ? void 0 : _knownRecord$boundari.processingStartedAt) === (boundaries === null || boundaries === void 0 ? void 0 : boundaries.processingStartedAt) && ((_knownRecord$boundari2 = knownRecord.boundaries) === null || _knownRecord$boundari2 === void 0 ? void 0 : _knownRecord$boundari2.processingEndedAt) === (boundaries === null || boundaries === void 0 ? void 0 : boundaries.processingEndedAt)) {
91
+ return false;
92
+ }
93
+ knownRecord.boundaries = boundaries;
94
+ this.attribute(knownRecord);
95
+ return true;
52
96
  }
53
97
 
54
- // Read only once the interaction has earned a place: naming its target walks the DOM, and
55
- // this runs while the page is already slow.
56
- const record = this.toRecord(interaction);
57
- if (previousIndex === -1) {
58
- this.records.push(record);
98
+ // Everything below builds a record out of `entry`, so it has to be an entry of this
99
+ // interaction. The tracker also reports an interaction whose boundaries moved because of an
100
+ // event that is no interaction of its own, and that event names something else entirely.
101
+ if (entry.interactionId !== interactionId) {
102
+ return false;
103
+ }
104
+ if (knownRecord) {
105
+ // An interaction measured as slower replaces itself rather than taking a second place.
106
+ this.records[index] = this.toRecord(entry, update);
59
107
  } else {
60
- this.records[previousIndex] = record;
108
+ const toBeatMs = this.records.length === MAX_RECORDS ? this.records[MAX_RECORDS - 1].latencyMs : MIN_LATENCY_MS;
109
+ if (latencyMs <= toBeatMs) {
110
+ return false;
111
+ }
112
+ this.records.push(this.toRecord(entry, update));
61
113
  }
62
- this.records.sort((a, b) => b.durationMs - a.durationMs);
114
+ this.records.sort((a, b) => b.latencyMs - a.latencyMs);
63
115
  this.records.splice(MAX_RECORDS);
116
+ return true;
64
117
  }
65
118
 
66
- /** @returns the records, slowest first, or nothing when no interaction was slow enough. */
119
+ /**
120
+ * Takes in the frames the browser has just reported and works out again what the frames say about
121
+ * every record — again, because the frames of one interaction can be reported in several batches
122
+ * and the first of them may hold neither its longest script nor all of its style and layout.
123
+ *
124
+ * @returns whether that changed what a snapshot would carry.
125
+ */
126
+ trackLongAnimationFrames(frames) {
127
+ this.frames.push(...frames);
128
+ let changed = false;
129
+ for (const record of this.records) {
130
+ changed = this.attribute(record) || changed;
131
+ }
132
+ return changed;
133
+ }
67
134
  snapshot() {
68
135
  if (this.records.length === 0) {
69
136
  return undefined;
70
137
  }
71
- return this.records.map(record => ({
72
- group: record.group,
73
- name: record.name,
74
- durationMs: record.durationMs,
75
- inputDelayMs: record.inputDelayMs,
76
- processingMs: record.processingMs,
77
- presentationDelayMs: record.presentationDelayMs,
78
- target: record.target
79
- }));
138
+ return this.records.map(record => {
139
+ var _record$attribution, _record$attribution2, _record$attribution3, _record$attribution4, _record$attribution5, _record$attribution6, _record$attribution7, _record$attribution8, _record$attribution9;
140
+ const boundaries = record.boundaries;
141
+ return {
142
+ group: record.group,
143
+ name: record.name,
144
+ durationMs: Math.round(record.latencyMs),
145
+ inputDelayMs: boundaries && Math.round(boundaries.processingStartedAt - boundaries.startedAt),
146
+ processingMs: boundaries && Math.round(boundaries.processingEndedAt - boundaries.processingStartedAt),
147
+ presentationDelayMs: boundaries && Math.round(boundaries.presentedAt - boundaries.processingEndedAt),
148
+ target: record.target,
149
+ functionName: (_record$attribution = record.attribution) === null || _record$attribution === void 0 ? void 0 : _record$attribution.functionName,
150
+ invokerType: (_record$attribution2 = record.attribution) === null || _record$attribution2 === void 0 ? void 0 : _record$attribution2.invokerType,
151
+ longestScriptMs: (_record$attribution3 = record.attribution) === null || _record$attribution3 === void 0 ? void 0 : _record$attribution3.longestScriptMs,
152
+ scriptName: (_record$attribution4 = record.attribution) === null || _record$attribution4 === void 0 ? void 0 : _record$attribution4.scriptName,
153
+ scriptSubpart: (_record$attribution5 = record.attribution) === null || _record$attribution5 === void 0 ? void 0 : _record$attribution5.scriptSubpart,
154
+ totalPaintDurationMs: (_record$attribution6 = record.attribution) === null || _record$attribution6 === void 0 ? void 0 : _record$attribution6.totalPaintDurationMs,
155
+ totalScriptDurationMs: (_record$attribution7 = record.attribution) === null || _record$attribution7 === void 0 ? void 0 : _record$attribution7.totalScriptDurationMs,
156
+ totalStyleAndLayoutDurationMs: (_record$attribution8 = record.attribution) === null || _record$attribution8 === void 0 ? void 0 : _record$attribution8.totalStyleAndLayoutDurationMs,
157
+ totalUnattributedDurationMs: (_record$attribution9 = record.attribution) === null || _record$attribution9 === void 0 ? void 0 : _record$attribution9.totalUnattributedDurationMs
158
+ };
159
+ });
160
+ }
161
+ toRecord(entry, update) {
162
+ var _update$group;
163
+ // The target is read only once the interaction has earned a place: naming it walks the DOM,
164
+ // and this runs while the page is already slow.
165
+ const record = {
166
+ attribution: undefined,
167
+ boundaries: update.boundaries,
168
+ interactionId: update.interactionId,
169
+ // An interaction the editor never reported an event for is not the editor's as far as we
170
+ // know.
171
+ group: (_update$group = update.group) !== null && _update$group !== void 0 ? _update$group : 'outsideEditor',
172
+ name: entry.name,
173
+ latencyMs: update.latencyMs,
174
+ target: this.describeTarget(entry.target)
175
+ };
176
+
177
+ // Frames reported before this entry already answer for it.
178
+ this.attribute(record);
179
+ return record;
180
+ }
181
+ attribute(record) {
182
+ const attribution = record.boundaries && this.attributionFor(record.boundaries);
183
+ if (!attribution) {
184
+ return false;
185
+ }
186
+ if (record.attribution && sameAttribution(record.attribution, attribution)) {
187
+ return false;
188
+ }
189
+ record.attribution = attribution;
190
+ return true;
80
191
  }
81
192
 
82
193
  /**
83
- * The phases are reported together or not at all: a browser that reports one reports all three,
84
- * and zeroes standing in for values we never had would read as an interaction that spent no time
85
- * anywhere. Each is clamped, because they come from timestamps the browser coarsens
86
- * independently, so a phase can come out just below zero.
194
+ * What the frames say about an interaction, attributed the way `web-vitals` attributes INP: every
195
+ * frame overlapping the interaction counts, the script that counts is the one with the longest
196
+ * part inside it, and style and layout is summed across those frames.
197
+ *
198
+ * @returns nothing when no frame overlaps the interaction — the browser reports frames above
199
+ * 50 ms only.
87
200
  */
88
- toRecord({
89
- entry,
90
- group,
91
- interactionId,
92
- latencyMs
93
- }) {
94
- const {
95
- processingStart,
96
- processingEnd,
97
- startTime
98
- } = entry;
99
- const measured = typeof processingStart === 'number' && typeof processingEnd === 'number';
100
- const phaseMs = durationMs => Math.max(0, Math.round(durationMs));
201
+ attributionFor(boundaries) {
202
+ var _longestScript, _longestScript2, _longestScript3;
203
+ let overlapped = false;
204
+ let lastFrameEndTime = 0;
205
+ let totalScriptDurationMs = 0;
206
+ let totalStyleAndLayoutDurationMs = 0;
207
+ let longestScript;
208
+ let longestScriptMs = 0;
209
+ for (const frame of this.frames) {
210
+ // Frames come in the order they were rendered, so once one starts after the interaction,
211
+ // so does every frame after it.
212
+ if (frame.startTime > boundaries.processingEndedAt) {
213
+ break;
214
+ }
215
+ const frameEndTime = frame.startTime + frame.duration;
216
+ if (frameEndTime < boundaries.startedAt) {
217
+ continue;
218
+ }
219
+ overlapped = true;
220
+ lastFrameEndTime = frameEndTime;
221
+ totalStyleAndLayoutDurationMs += this.styleAndLayoutOf(frame);
222
+ for (const script of (_frame$scripts = frame.scripts) !== null && _frame$scripts !== void 0 ? _frame$scripts : []) {
223
+ var _frame$scripts, _script$forcedStyleAn;
224
+ const scriptEndTime = script.startTime + script.duration;
225
+ if (scriptEndTime < boundaries.startedAt) {
226
+ continue;
227
+ }
228
+ const insideInteractionMs = scriptEndTime - Math.max(boundaries.startedAt, script.startTime);
229
+ // `forcedStyleAndLayoutDuration` carries no timestamps, so the part of it inside the
230
+ // interaction is apportioned. It counts as style and layout rather than script time,
231
+ // the same split DevTools shows.
232
+ const forcedInsideMs = script.duration ? insideInteractionMs / script.duration * ((_script$forcedStyleAn = script.forcedStyleAndLayoutDuration) !== null && _script$forcedStyleAn !== void 0 ? _script$forcedStyleAn : 0) : 0;
233
+ totalScriptDurationMs += insideInteractionMs - forcedInsideMs;
234
+ totalStyleAndLayoutDurationMs += forcedInsideMs;
235
+ if (insideInteractionMs > longestScriptMs) {
236
+ longestScript = script;
237
+ longestScriptMs = insideInteractionMs;
238
+ }
239
+ }
240
+ }
241
+ if (!overlapped) {
242
+ return undefined;
243
+ }
244
+
245
+ // What the browser did after the last frame of the interaction, so it only counts when that
246
+ // frame ended no earlier than the handlers did.
247
+ const totalPaintDurationMs = lastFrameEndTime >= boundaries.processingEndedAt ? Math.max(0, boundaries.presentedAt - lastFrameEndTime) : 0;
248
+ // Every total is brought to what it is reported as before this subtraction, so that the four
249
+ // of them add up to the latency rather than to more than it: a frame whose render phase runs
250
+ // past the interaction would otherwise leave a negative here to be counted twice.
251
+ totalScriptDurationMs = Math.max(0, totalScriptDurationMs);
252
+ totalStyleAndLayoutDurationMs = Math.max(0, totalStyleAndLayoutDurationMs);
253
+ // Whatever is left of the latency: the thread was busy with something the frames attributed
254
+ // to no script, to no style and layout, and to no paint.
255
+ const totalUnattributedDurationMs = Math.max(0, boundaries.presentedAt - boundaries.startedAt - totalScriptDurationMs - totalStyleAndLayoutDurationMs - totalPaintDurationMs);
101
256
  return {
102
- interactionId,
103
- group,
104
- name: entry.name,
105
- durationMs: Math.round(latencyMs),
106
- inputDelayMs: measured ? phaseMs(processingStart - startTime) : undefined,
107
- processingMs: measured ? phaseMs(processingEnd - processingStart) : undefined,
108
- presentationDelayMs: measured ? phaseMs(startTime + latencyMs - processingEnd) : undefined,
109
- target: this.describeTarget(entry.target)
257
+ functionName: this.truncated((_longestScript = longestScript) === null || _longestScript === void 0 ? void 0 : _longestScript.sourceFunctionName),
258
+ invokerType: this.truncated((_longestScript2 = longestScript) === null || _longestScript2 === void 0 ? void 0 : _longestScript2.invokerType),
259
+ longestScriptMs: longestScript && Math.round(longestScriptMs),
260
+ scriptName: this.truncated(this.fileName((_longestScript3 = longestScript) === null || _longestScript3 === void 0 ? void 0 : _longestScript3.sourceURL)),
261
+ scriptSubpart: longestScript && this.subpartOf(longestScript, boundaries),
262
+ totalPaintDurationMs: Math.round(totalPaintDurationMs),
263
+ totalScriptDurationMs: Math.round(totalScriptDurationMs),
264
+ totalStyleAndLayoutDurationMs: Math.round(totalStyleAndLayoutDurationMs),
265
+ totalUnattributedDurationMs: Math.round(totalUnattributedDurationMs)
110
266
  };
111
267
  }
112
268
 
269
+ /**
270
+ * Style, layout and paint of the frame, which the browser reports as starting at 0 when the
271
+ * frame did none.
272
+ */
273
+ styleAndLayoutOf(frame) {
274
+ const {
275
+ styleAndLayoutStart
276
+ } = frame;
277
+ if (typeof styleAndLayoutStart !== 'number' || styleAndLayoutStart === 0) {
278
+ return 0;
279
+ }
280
+ const frameEndTime = frame.startTime + frame.duration;
281
+ return Math.max(0, frameEndTime - styleAndLayoutStart);
282
+ }
283
+
284
+ /** Which phase of the interaction the script ran in, by where it started. */
285
+ subpartOf(script, boundaries) {
286
+ if (script.startTime < boundaries.processingStartedAt) {
287
+ return 'inputDelay';
288
+ }
289
+ return script.startTime >= boundaries.processingEndedAt ? 'presentationDelay' : 'processing';
290
+ }
291
+ truncated(name) {
292
+ return name ? name.slice(0, MAX_NAME_LENGTH) : undefined;
293
+ }
294
+
295
+ /**
296
+ * The file as the browser named it, content hash and all: that is what identifies the artefact
297
+ * and its source map, and a query can be grouped away downstream.
298
+ */
299
+ fileName(sourceURL) {
300
+ if (!sourceURL) {
301
+ return undefined;
302
+ }
303
+ const path = sourceURL.split(QUERY_OR_HASH)[0];
304
+ return path.slice(path.lastIndexOf('/') + 1);
305
+ }
306
+
113
307
  /**
114
308
  * Names the element an interaction happened on — `div[data-vc="x"] > p > span`, outermost first.
115
309
  * The path climbs until an element carries an allow-listed attribute, because that is what says
@@ -0,0 +1,35 @@
1
+ import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
2
+ import _createClass from "@babel/runtime/helpers/createClass";
3
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
+ /** An array that forgets its oldest item once it holds more than `limit`. */
5
+ export var BoundedList = /*#__PURE__*/function () {
6
+ function BoundedList(limit) {
7
+ _classCallCheck(this, BoundedList);
8
+ _defineProperty(this, "items", []);
9
+ this.limit = limit;
10
+ }
11
+ return _createClass(BoundedList, [{
12
+ key: "push",
13
+ value: function push() {
14
+ var _this$items;
15
+ (_this$items = this.items).push.apply(_this$items, arguments);
16
+ // Negative when there is still room, and `splice` then removes nothing.
17
+ this.items.splice(0, this.items.length - this.limit);
18
+ }
19
+ }, {
20
+ key: Symbol.iterator,
21
+ value: function value() {
22
+ return this.items[Symbol.iterator]();
23
+ }
24
+ }, {
25
+ key: "findLast",
26
+ value: function findLast(matches) {
27
+ for (var index = this.items.length - 1; index >= 0; index -= 1) {
28
+ if (matches(this.items[index])) {
29
+ return this.items[index];
30
+ }
31
+ }
32
+ return undefined;
33
+ }
34
+ }]);
35
+ }();
@@ -0,0 +1,35 @@
1
+ import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
2
+ import _createClass from "@babel/runtime/helpers/createClass";
3
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
+ /** A `Map` that forgets its oldest entry once it holds more than `limit`. */
5
+ export var BoundedMap = /*#__PURE__*/function () {
6
+ function BoundedMap(limit) {
7
+ _classCallCheck(this, BoundedMap);
8
+ _defineProperty(this, "entries", new Map());
9
+ this.limit = limit;
10
+ }
11
+ return _createClass(BoundedMap, [{
12
+ key: "get",
13
+ value: function get(key) {
14
+ return this.entries.get(key);
15
+ }
16
+ }, {
17
+ key: "forEach",
18
+ value: function forEach(visit) {
19
+ this.entries.forEach(visit);
20
+ }
21
+ }, {
22
+ key: "set",
23
+ value: function set(key, value) {
24
+ this.entries.delete(key);
25
+ this.entries.set(key, value);
26
+ if (this.entries.size <= this.limit) {
27
+ return;
28
+ }
29
+ var oldest = this.entries.keys().next();
30
+ if (!oldest.done) {
31
+ this.entries.delete(oldest.value);
32
+ }
33
+ }
34
+ }]);
35
+ }();
@@ -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: {
@@ -5,7 +5,6 @@ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol
5
5
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
6
6
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
7
7
  import { bucketKeyForMs } from './bucket-boundaries';
8
-
9
8
  /**
10
9
  * Latencies are counted per 8 ms, the resolution Event Timing reports durations at. Every
11
10
  * latency is rounded up to this step on the way in, which caps the number of distinct values
@@ -25,10 +24,10 @@ var REPORTED_QUANTILES = [0.9, 0.98];
25
24
  * count, the sum, the maximum and the percentiles — is derived from that map when a snapshot
26
25
  * is taken. Nothing is computed while interactions arrive.
27
26
  *
28
- * The two counters count different populations: `add` takes the interactions Event Timing
29
- * measured, `countTotal` takes all of them, including the ones below the 16 ms reporting threshold
30
- * it never delivers. So `totalCount >= observedCount`, and the difference is how many were too
31
- * fast to be measured.
27
+ * The two counters count different populations: `trackInteractionUpdate` takes the interactions
28
+ * Event Timing measured, `countTotal` takes all of them, including the ones below the 16 ms
29
+ * reporting threshold it never delivers. So `totalCount >= observedCount`, and the difference is how
30
+ * many were too fast to be measured.
32
31
  */
33
32
  export var InteractionGroup = /*#__PURE__*/function () {
34
33
  function InteractionGroup() {
@@ -37,6 +36,22 @@ export var InteractionGroup = /*#__PURE__*/function () {
37
36
  _defineProperty(this, "totalCount", 0);
38
37
  }
39
38
  return _createClass(InteractionGroup, [{
39
+ key: "trackInteractionUpdate",
40
+ value:
41
+ /**
42
+ * Takes in what the tracker now says about an interaction: a new one is counted, and one measured
43
+ * again moves the count it already has.
44
+ *
45
+ * @returns whether the group changed.
46
+ */
47
+ function trackInteractionUpdate(update) {
48
+ if (update.type === 'new') {
49
+ this.add(update.latencyMs);
50
+ return true;
51
+ }
52
+ return this.remeasure(update.previousLatencyMs, update.latencyMs);
53
+ }
54
+ }, {
40
55
  key: "add",
41
56
  value: function add(latencyMs) {
42
57
  this.increment(latencyMs);
@@ -51,12 +66,23 @@ export var InteractionGroup = /*#__PURE__*/function () {
51
66
  value: function countTotal() {
52
67
  this.totalCount += 1;
53
68
  }
69
+
70
+ /**
71
+ * @returns whether the count moved, which is `false` when both latencies fall in the step the
72
+ * interaction is already counted in — including when the interaction was measured no slower at
73
+ * all and only its boundaries moved.
74
+ */
54
75
  }, {
55
76
  key: "remeasure",
56
77
  value: function remeasure(previousLatencyMs, latencyMs) {
78
+ if (this.roundLatencyUp(previousLatencyMs) === this.roundLatencyUp(latencyMs)) {
79
+ return false;
80
+ }
81
+
57
82
  // Moved rather than counted again: the count belongs to the same interaction.
58
83
  this.decrement(previousLatencyMs);
59
84
  this.increment(latencyMs);
85
+ return true;
60
86
  }
61
87
 
62
88
  /**