@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.
- package/CHANGELOG.md +27 -0
- package/README.md +79 -0
- package/afm-cc/tsconfig.json +3 -0
- package/afm-products/tsconfig.json +3 -0
- package/dist/cjs/collections/bounded-list.js +42 -0
- package/dist/cjs/collections/bounded-map.js +42 -0
- package/dist/cjs/collector/interaction-events.js +0 -7
- package/dist/cjs/collector/interaction-group.js +31 -4
- package/dist/cjs/collector/interaction-tracker.js +210 -68
- package/dist/cjs/collector/interactivity-collector.js +52 -22
- package/dist/cjs/collector/interactivity-session.js +3 -0
- package/dist/cjs/collector/long-animation-frame-observer.js +62 -0
- package/dist/cjs/collector/slow-interaction-list.js +413 -0
- package/dist/es2019/collections/bounded-list.js +24 -0
- package/dist/es2019/collections/bounded-map.js +25 -0
- package/dist/es2019/collector/interaction-events.js +0 -7
- package/dist/es2019/collector/interaction-group.js +28 -5
- package/dist/es2019/collector/interaction-tracker.js +201 -55
- package/dist/es2019/collector/interactivity-collector.js +35 -20
- package/dist/es2019/collector/interactivity-session.js +3 -0
- package/dist/es2019/collector/long-animation-frame-observer.js +42 -0
- package/dist/es2019/collector/slow-interaction-list.js +342 -0
- package/dist/esm/collections/bounded-list.js +35 -0
- package/dist/esm/collections/bounded-map.js +35 -0
- package/dist/esm/collector/interaction-events.js +0 -7
- package/dist/esm/collector/interaction-group.js +31 -5
- package/dist/esm/collector/interaction-tracker.js +210 -68
- package/dist/esm/collector/interactivity-collector.js +52 -22
- package/dist/esm/collector/interactivity-session.js +3 -0
- package/dist/esm/collector/long-animation-frame-observer.js +55 -0
- package/dist/esm/collector/slow-interaction-list.js +407 -0
- package/dist/types/analytics/interactivity-snapshot.d.ts +65 -0
- package/dist/types/collections/bounded-list.d.ts +9 -0
- package/dist/types/collections/bounded-map.d.ts +9 -0
- package/dist/types/collector/interaction-events.d.ts +1 -7
- package/dist/types/collector/interaction-group.d.ts +18 -5
- package/dist/types/collector/interaction-tracker.d.ts +67 -13
- package/dist/types/collector/interactivity-collector.d.ts +3 -0
- package/dist/types/collector/interactivity-session.d.ts +2 -0
- package/dist/types/collector/long-animation-frame-observer.d.ts +28 -0
- package/dist/types/collector/slow-interaction-list.d.ts +64 -0
- package/docs/0-intro.tsx +2 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-interactivity
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`1b78d9e80b1bd`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/1b78d9e80b1bd) -
|
|
8
|
+
Say where the latency of the slowest interactions went in the `editor interactivity` event, from
|
|
9
|
+
the Long Animation Frames they ran in and the way `web-vitals` attributes INP. `longestScriptMs`,
|
|
10
|
+
`functionName`, `scriptName`, `invokerType` and `scriptSubpart` describe the script that ran the
|
|
11
|
+
longest while the user waited, and `totalScriptDurationMs`, `totalStyleAndLayoutDurationMs`,
|
|
12
|
+
`totalPaintDurationMs` and `totalUnattributedDurationMs` divide the latency between script, style
|
|
13
|
+
and layout, paint, and what the frames explain nothing about. Behind the
|
|
14
|
+
platform_editor_editor_interactivity_slowest experiment.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
|
|
20
|
+
## 1.1.0
|
|
21
|
+
|
|
22
|
+
### Minor Changes
|
|
23
|
+
|
|
24
|
+
- [`ccd1b52723728`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/ccd1b52723728) -
|
|
25
|
+
Report the slowest interactions of the session in the `editor interactivity` event — their group,
|
|
26
|
+
event type, latency, timing phases and a sanitised target — so a regression the histograms show
|
|
27
|
+
can be traced to what was interacted with. Behind the platform_editor_editor_interactivity_slowest
|
|
28
|
+
experiment.
|
|
29
|
+
|
|
3
30
|
## 1.0.0
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -31,6 +31,85 @@ interactions were slow, so this plugin keeps bucketed counts instead.
|
|
|
31
31
|
bucket means zero. The boundaries are versioned by `schema`.
|
|
32
32
|
- Snapshots are session-to-date, so a cohort query takes the highest `seq` per
|
|
33
33
|
`interactivitySessionId` and then sums bucket counts.
|
|
34
|
+
- `slowest` — up to five of the slowest interactions of the session, slowest first. See below.
|
|
35
|
+
|
|
36
|
+
## The slowest interactions
|
|
37
|
+
|
|
38
|
+
The histograms count how many interactions were slow. They cannot say what was interacted with, so
|
|
39
|
+
a regression they show cannot be diagnosed from telemetry alone. Each record carries the group and
|
|
40
|
+
the event type, the latency, the phases it divides into — input delay, processing, presentation
|
|
41
|
+
delay — a name for the target, and what the frame it ran in says about it.
|
|
42
|
+
|
|
43
|
+
Only interactions above 200 ms, the Google INP "good" threshold, are candidates, and `slowest` is
|
|
44
|
+
left out of the event when none crossed it, which is the common case. That is also what keeps the
|
|
45
|
+
cost down: a target is named only for the few interactions slow enough to be recorded, never for
|
|
46
|
+
every one the browser reports.
|
|
47
|
+
|
|
48
|
+
Records are kept per interaction, because an interaction's latency grows as later entries arrive:
|
|
49
|
+
one that is already recorded is replaced by its slower self instead of taking a second place. Its
|
|
50
|
+
attribution comes from the entry that measured it at its slowest, read as that entry arrives —
|
|
51
|
+
`entry.target` is `null` once the element has left the document, so there is no reading it later.
|
|
52
|
+
|
|
53
|
+
The target is a short path of tag names plus the first allow-listed attribute above it, such as
|
|
54
|
+
`div[data-vc="editor"] > p > span`. Nothing in it is document content: a tag name comes from the
|
|
55
|
+
schema, and the only attributes read are ones we put there ourselves. Ids, roles, class names, text
|
|
56
|
+
and accessibility labels are all left out, and the path is bounded in both depth and length.
|
|
57
|
+
|
|
58
|
+
The rest of a record comes from the Long Animation Frames of the interaction, attributed the way
|
|
59
|
+
[`web-vitals` attributes INP](https://github.com/GoogleChrome/web-vitals/blob/main/src/attribution/onINP.ts):
|
|
60
|
+
every frame overlapping the interaction counts, and the script that counts is the one with the
|
|
61
|
+
longest part inside it.
|
|
62
|
+
|
|
63
|
+
- `longestScriptMs`, `functionName`, `scriptName`, `invokerType` and `scriptSubpart` describe that one
|
|
64
|
+
script — how much of it fell inside the interaction, the function it ran in, the file it came from
|
|
65
|
+
as the browser named it without its origin or query, what ran it, and which phase of the
|
|
66
|
+
interaction it ran in. The phase is what keeps a script named for an interaction that waited on the
|
|
67
|
+
main thread from being mistaken for one its handlers ran.
|
|
68
|
+
- `totalScriptDurationMs`, `totalStyleAndLayoutDurationMs`, `totalPaintDurationMs` and
|
|
69
|
+
`totalUnattributedDurationMs` divide the latency itself across every frame of the interaction. The
|
|
70
|
+
time a script forced into style and layout counts as style and layout rather than as script, the
|
|
71
|
+
same split DevTools shows. `totalUnattributedDurationMs` is what the frames explain nothing about,
|
|
72
|
+
and it is not idle time: the browser reports no frame under 50 ms, so the work of those lands here
|
|
73
|
+
as well.
|
|
74
|
+
|
|
75
|
+
A record is attributed as soon as a frame of it is known, and worked out again on every batch of
|
|
76
|
+
frames after that. The browser reports a frame in no fixed order — it can deliver one after the entry
|
|
77
|
+
measuring the interaction that ran in it, and the frames of one interaction in several batches — so a
|
|
78
|
+
record can go out without these fields, or with what one frame said, and be corrected in a later
|
|
79
|
+
snapshot of the same session. That is another reason a query takes the highest `seq`. All of them are
|
|
80
|
+
absent when the browser reported no frame at all, which it does for frames under 50 ms and outside
|
|
81
|
+
Chromium, and the fields describing one script are absent when no script of those frames overlapped
|
|
82
|
+
the interaction.
|
|
83
|
+
|
|
84
|
+
The phases are the gaps between four moments: the user acted, the interaction's handlers started
|
|
85
|
+
running, they finished, the screen updated. The first and the last come from the entry that measured
|
|
86
|
+
the interaction at its slowest. The two in between come from the paint that presented that entry,
|
|
87
|
+
which is every event whose `startTime + duration` agrees within 8 ms — the rounding Event Timing
|
|
88
|
+
applies to `duration`, and the only thing it says about which paint presented an event. So a
|
|
89
|
+
`pointerover` handler that was still running when the user clicked reads as processing of the click
|
|
90
|
+
rather than as time the click waited for nothing, which is also the split `web-vitals` reports. The
|
|
91
|
+
four are clamped as it clamps them, so they stay in order whatever the browser reported.
|
|
92
|
+
|
|
93
|
+
A paint keeps growing while the browser reports the rest of the events it presented, so an
|
|
94
|
+
interaction is reported again whenever its paint grew — including when what grew it was another
|
|
95
|
+
interaction presented by the same paint, or an event that is no interaction at all. The last handlers
|
|
96
|
+
of a paint are usually reported after the event that measured the interaction, so the phases of a
|
|
97
|
+
record are corrected in a later snapshot of the same session.
|
|
98
|
+
|
|
99
|
+
One thing we do not copy: their style and layout total takes the frame's render phase unguarded; we
|
|
100
|
+
report zero for a frame the browser said did none, which their formula would otherwise turn into an
|
|
101
|
+
absolute timestamp.
|
|
102
|
+
|
|
103
|
+
An interaction that is not the editor's is recorded as `group: 'outsideEditor'`, so the records also
|
|
104
|
+
answer whether the slow interactions of a session were the editor's at all. That group is not the
|
|
105
|
+
`page` histogram: `page` counts the editor's interactions as well, `outsideEditor` counts only the
|
|
106
|
+
ones that are not.
|
|
107
|
+
|
|
108
|
+
Collecting them is its own experiment, `platform_editor_editor_interactivity_slowest`, read where a
|
|
109
|
+
session decides whether to keep them rather than where the plugin is added to a preset: the plugin
|
|
110
|
+
only exists when the experiment reporting the histograms is on, so the exposure lands on the
|
|
111
|
+
sessions that already send the event, and the cost of the records stays separable from the cost of
|
|
112
|
+
the event carrying them. Nothing is collected, named or sent when it is off.
|
|
34
113
|
|
|
35
114
|
## The editor groups
|
|
36
115
|
|
package/afm-cc/tsconfig.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.BoundedList = void 0;
|
|
8
|
+
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
|
|
9
|
+
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
|
|
10
|
+
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
11
|
+
/** An array that forgets its oldest item once it holds more than `limit`. */
|
|
12
|
+
var BoundedList = exports.BoundedList = /*#__PURE__*/function () {
|
|
13
|
+
function BoundedList(limit) {
|
|
14
|
+
(0, _classCallCheck2.default)(this, BoundedList);
|
|
15
|
+
(0, _defineProperty2.default)(this, "items", []);
|
|
16
|
+
this.limit = limit;
|
|
17
|
+
}
|
|
18
|
+
return (0, _createClass2.default)(BoundedList, [{
|
|
19
|
+
key: "push",
|
|
20
|
+
value: function push() {
|
|
21
|
+
var _this$items;
|
|
22
|
+
(_this$items = this.items).push.apply(_this$items, arguments);
|
|
23
|
+
// Negative when there is still room, and `splice` then removes nothing.
|
|
24
|
+
this.items.splice(0, this.items.length - this.limit);
|
|
25
|
+
}
|
|
26
|
+
}, {
|
|
27
|
+
key: Symbol.iterator,
|
|
28
|
+
value: function value() {
|
|
29
|
+
return this.items[Symbol.iterator]();
|
|
30
|
+
}
|
|
31
|
+
}, {
|
|
32
|
+
key: "findLast",
|
|
33
|
+
value: function findLast(matches) {
|
|
34
|
+
for (var index = this.items.length - 1; index >= 0; index -= 1) {
|
|
35
|
+
if (matches(this.items[index])) {
|
|
36
|
+
return this.items[index];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}]);
|
|
42
|
+
}();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.BoundedMap = void 0;
|
|
8
|
+
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
|
|
9
|
+
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
|
|
10
|
+
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
11
|
+
/** A `Map` that forgets its oldest entry once it holds more than `limit`. */
|
|
12
|
+
var BoundedMap = exports.BoundedMap = /*#__PURE__*/function () {
|
|
13
|
+
function BoundedMap(limit) {
|
|
14
|
+
(0, _classCallCheck2.default)(this, BoundedMap);
|
|
15
|
+
(0, _defineProperty2.default)(this, "entries", new Map());
|
|
16
|
+
this.limit = limit;
|
|
17
|
+
}
|
|
18
|
+
return (0, _createClass2.default)(BoundedMap, [{
|
|
19
|
+
key: "get",
|
|
20
|
+
value: function get(key) {
|
|
21
|
+
return this.entries.get(key);
|
|
22
|
+
}
|
|
23
|
+
}, {
|
|
24
|
+
key: "forEach",
|
|
25
|
+
value: function forEach(visit) {
|
|
26
|
+
this.entries.forEach(visit);
|
|
27
|
+
}
|
|
28
|
+
}, {
|
|
29
|
+
key: "set",
|
|
30
|
+
value: function set(key, value) {
|
|
31
|
+
this.entries.delete(key);
|
|
32
|
+
this.entries.set(key, value);
|
|
33
|
+
if (this.entries.size <= this.limit) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
var oldest = this.entries.keys().next();
|
|
37
|
+
if (!oldest.done) {
|
|
38
|
+
this.entries.delete(oldest.value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}]);
|
|
42
|
+
}();
|
|
@@ -5,13 +5,6 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.INTERACTION_EVENT_TYPES = void 0;
|
|
7
7
|
exports.interactionEventKind = interactionEventKind;
|
|
8
|
-
/**
|
|
9
|
-
* The editor group an interaction belongs to, which is also the field the event reports it in.
|
|
10
|
-
*
|
|
11
|
-
* `editorOther` is the remainder — an interaction the browser counts that is neither typing nor
|
|
12
|
-
* pointing — so the three groups together cover whatever the browser calls an interaction.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
8
|
/**
|
|
16
9
|
* The events an interaction is made of, and the group they belong to. Both sides of the collector
|
|
17
10
|
* read this, so counting and grouping cannot disagree.
|
|
@@ -31,10 +31,10 @@ var REPORTED_QUANTILES = [0.9, 0.98];
|
|
|
31
31
|
* count, the sum, the maximum and the percentiles — is derived from that map when a snapshot
|
|
32
32
|
* is taken. Nothing is computed while interactions arrive.
|
|
33
33
|
*
|
|
34
|
-
* The two counters count different populations: `
|
|
35
|
-
* measured, `countTotal` takes all of them, including the ones below the 16 ms
|
|
36
|
-
* it never delivers. So `totalCount >= observedCount`, and the difference is how
|
|
37
|
-
* fast to be measured.
|
|
34
|
+
* The two counters count different populations: `trackInteractionUpdate` takes the interactions
|
|
35
|
+
* Event Timing measured, `countTotal` takes all of them, including the ones below the 16 ms
|
|
36
|
+
* reporting threshold it never delivers. So `totalCount >= observedCount`, and the difference is how
|
|
37
|
+
* many were too fast to be measured.
|
|
38
38
|
*/
|
|
39
39
|
var InteractionGroup = exports.InteractionGroup = /*#__PURE__*/function () {
|
|
40
40
|
function InteractionGroup() {
|
|
@@ -43,6 +43,22 @@ var InteractionGroup = exports.InteractionGroup = /*#__PURE__*/function () {
|
|
|
43
43
|
(0, _defineProperty2.default)(this, "totalCount", 0);
|
|
44
44
|
}
|
|
45
45
|
return (0, _createClass2.default)(InteractionGroup, [{
|
|
46
|
+
key: "trackInteractionUpdate",
|
|
47
|
+
value:
|
|
48
|
+
/**
|
|
49
|
+
* Takes in what the tracker now says about an interaction: a new one is counted, and one measured
|
|
50
|
+
* again moves the count it already has.
|
|
51
|
+
*
|
|
52
|
+
* @returns whether the group changed.
|
|
53
|
+
*/
|
|
54
|
+
function trackInteractionUpdate(update) {
|
|
55
|
+
if (update.type === 'new') {
|
|
56
|
+
this.add(update.latencyMs);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return this.remeasure(update.previousLatencyMs, update.latencyMs);
|
|
60
|
+
}
|
|
61
|
+
}, {
|
|
46
62
|
key: "add",
|
|
47
63
|
value: function add(latencyMs) {
|
|
48
64
|
this.increment(latencyMs);
|
|
@@ -57,12 +73,23 @@ var InteractionGroup = exports.InteractionGroup = /*#__PURE__*/function () {
|
|
|
57
73
|
value: function countTotal() {
|
|
58
74
|
this.totalCount += 1;
|
|
59
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @returns whether the count moved, which is `false` when both latencies fall in the step the
|
|
79
|
+
* interaction is already counted in — including when the interaction was measured no slower at
|
|
80
|
+
* all and only its boundaries moved.
|
|
81
|
+
*/
|
|
60
82
|
}, {
|
|
61
83
|
key: "remeasure",
|
|
62
84
|
value: function remeasure(previousLatencyMs, latencyMs) {
|
|
85
|
+
if (this.roundLatencyUp(previousLatencyMs) === this.roundLatencyUp(latencyMs)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
63
89
|
// Moved rather than counted again: the count belongs to the same interaction.
|
|
64
90
|
this.decrement(previousLatencyMs);
|
|
65
91
|
this.increment(latencyMs);
|
|
92
|
+
return true;
|
|
66
93
|
}
|
|
67
94
|
|
|
68
95
|
/**
|
|
@@ -5,23 +5,56 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
7
|
exports.InteractionTracker = void 0;
|
|
8
|
+
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
|
|
8
9
|
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
|
|
9
10
|
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
|
|
10
11
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
12
|
+
var _boundedList = require("../collections/bounded-list");
|
|
13
|
+
var _boundedMap = require("../collections/bounded-map");
|
|
11
14
|
var _interactionEvents = require("./interaction-events");
|
|
12
|
-
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
|
|
13
|
-
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
14
|
-
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
15
15
|
/**
|
|
16
|
-
* The Event Timing fields this package reads
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* The Event Timing fields this package reads, declared optional so that a `PerformanceEntry` from
|
|
17
|
+
* `getEntries()` is assignable without a cast — `interactionId` is missing from the DOM typings'
|
|
18
|
+
* `PerformanceEventTiming` altogether, and the rest are only on it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One paint, and the processing of every event it presented.
|
|
23
|
+
*
|
|
24
|
+
* One paint can present several events, and the handlers of all of them ran before it: a
|
|
25
|
+
* `pointerover` handler that was still running when the user clicked held up the paint that showed
|
|
26
|
+
* the click. So an interaction's processing is the processing of its whole paint, not of its own
|
|
27
|
+
* events only — otherwise a handler that is not its own reads as time the user waited for nothing.
|
|
28
|
+
*
|
|
29
|
+
* Event Timing gives a paint no identity. The only thing an entry says about it is
|
|
30
|
+
* `startTime + duration`, the moment it happened, so that is what identifies it.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The paint an entry was presented by, and whether the entry moved the processing that paint covers.
|
|
35
|
+
* An entry whose handlers ran inside what the paint already covered changes nothing for any
|
|
36
|
+
* interaction reading its boundaries from it.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The four moments an interaction's latency divides at, in order: the user acted, its handlers
|
|
41
|
+
* started running, they finished, the screen updated.
|
|
19
42
|
*/
|
|
20
43
|
|
|
21
44
|
/**
|
|
22
45
|
* What an entry did to the interaction it belongs to: either it is the first entry of a new
|
|
23
|
-
* interaction, or it
|
|
24
|
-
*
|
|
46
|
+
* interaction, or it changed an interaction already known. Both carry the editor group of the
|
|
47
|
+
* interaction, if it is one of the editor's, and the boundaries it now has.
|
|
48
|
+
*
|
|
49
|
+
* A `remeasured` where `previousLatencyMs` equals `latencyMs` is an interaction whose latency stayed
|
|
50
|
+
* as it was and whose boundaries moved: the entry ran in the same paint without being the slowest
|
|
51
|
+
* of them.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What is kept per interaction. The boundaries are not among these: they are derived from the paint
|
|
56
|
+
* whenever the interaction is reported, because the paint keeps growing as the browser reports the
|
|
57
|
+
* remaining events it presented.
|
|
25
58
|
*/
|
|
26
59
|
|
|
27
60
|
/**
|
|
@@ -30,11 +63,17 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
|
|
|
30
63
|
* anything older than the last few hundred is not needed.
|
|
31
64
|
*/
|
|
32
65
|
var MAX_TRACKED = 256;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* How many paints entries can still be placed in. The entries of a paint arrive within a batch or
|
|
69
|
+
* two of each other, so this only has to cover the paints in flight; it is what `web-vitals` keeps.
|
|
70
|
+
*/
|
|
71
|
+
var MAX_RECENT_PAINTS = 10;
|
|
33
72
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
73
|
+
* Event Timing rounds `duration` down to 8 ms, so two events presented by one paint report that
|
|
74
|
+
* paint up to this far apart — and nothing else in Event Timing says they share it.
|
|
36
75
|
*/
|
|
37
|
-
var
|
|
76
|
+
var PRESENTATION_ROUNDING_MS = 8;
|
|
38
77
|
|
|
39
78
|
/**
|
|
40
79
|
* Identifies the event an entry measured: an entry's `startTime` is that event's timestamp and its
|
|
@@ -60,6 +99,9 @@ function eventKey(type, timeStamp) {
|
|
|
60
99
|
*
|
|
61
100
|
* The editor's events answer what an entry cannot: which interactions were with the editor, and
|
|
62
101
|
* how many there were, including the ones below the Event Timing reporting threshold.
|
|
102
|
+
*
|
|
103
|
+
* Every entry is also placed in the paint that presented it, which is what says how an
|
|
104
|
+
* interaction's latency divides into waiting, processing and presentation. See `Paint`.
|
|
63
105
|
*/
|
|
64
106
|
var InteractionTracker = exports.InteractionTracker = /*#__PURE__*/function () {
|
|
65
107
|
/**
|
|
@@ -72,8 +114,9 @@ var InteractionTracker = exports.InteractionTracker = /*#__PURE__*/function () {
|
|
|
72
114
|
function InteractionTracker() {
|
|
73
115
|
var startsAfterInteractionId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
|
|
74
116
|
(0, _classCallCheck2.default)(this, InteractionTracker);
|
|
75
|
-
(0, _defineProperty2.default)(this, "interactions", new
|
|
76
|
-
(0, _defineProperty2.default)(this, "groupByEvent", new
|
|
117
|
+
(0, _defineProperty2.default)(this, "interactions", new _boundedMap.BoundedMap(MAX_TRACKED));
|
|
118
|
+
(0, _defineProperty2.default)(this, "groupByEvent", new _boundedMap.BoundedMap(MAX_TRACKED));
|
|
119
|
+
(0, _defineProperty2.default)(this, "recentPaints", new _boundedList.BoundedList(MAX_RECENT_PAINTS));
|
|
77
120
|
(0, _defineProperty2.default)(this, "highestInteractionId", 0);
|
|
78
121
|
this.startsAfterInteractionId = startsAfterInteractionId;
|
|
79
122
|
}
|
|
@@ -88,58 +131,67 @@ var InteractionTracker = exports.InteractionTracker = /*#__PURE__*/function () {
|
|
|
88
131
|
/**
|
|
89
132
|
* Merges an entry into the interaction it belongs to.
|
|
90
133
|
*
|
|
91
|
-
* @returns what that
|
|
92
|
-
*
|
|
134
|
+
* @returns what that changed about the interactions this tracker knows, the entry's own first.
|
|
135
|
+
* More than one of them when the paint the entry ran in presented several.
|
|
93
136
|
*/
|
|
94
137
|
}, {
|
|
95
138
|
key: "merge",
|
|
96
139
|
value: function merge(entry) {
|
|
140
|
+
if (!Number.isFinite(entry.duration) || entry.duration < 0) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
var placement = this.paintOf(entry);
|
|
144
|
+
var paint = placement === null || placement === void 0 ? void 0 : placement.paint;
|
|
97
145
|
var interactionId = entry.interactionId;
|
|
98
|
-
|
|
146
|
+
|
|
147
|
+
// Reported only when the entry grew the paint, because otherwise nothing an interaction reads
|
|
148
|
+
// from it moved. Every interaction the paint presented is here, not only the entry's own: a
|
|
149
|
+
// `first-input` or non-interaction event reports `interactionId` 0 and has none of its own,
|
|
150
|
+
// and a second press of the same paint moved where the first one spent its latency.
|
|
151
|
+
//
|
|
152
|
+
// The check below is neither reached with a `0` nor needed: the interactions reported are the
|
|
153
|
+
// ones this tracker holds, and the only way into that map is past the check.
|
|
154
|
+
var remeasuredOthers = placement !== null && placement !== void 0 && placement.grew ? this.remeasuredUpdatesIn(placement.paint, {
|
|
155
|
+
except: interactionId
|
|
156
|
+
}) : [];
|
|
99
157
|
if (!interactionId) {
|
|
100
|
-
return
|
|
158
|
+
return remeasuredOthers;
|
|
101
159
|
}
|
|
102
160
|
if (interactionId <= this.startsAfterInteractionId) {
|
|
103
|
-
|
|
161
|
+
// The entry belongs to the session before this one, but its handlers still ran before a
|
|
162
|
+
// paint of this one.
|
|
163
|
+
return remeasuredOthers;
|
|
104
164
|
}
|
|
105
165
|
this.highestInteractionId = Math.max(this.highestInteractionId, interactionId);
|
|
106
|
-
|
|
107
|
-
// Every latency is counted through here, so this is where one that cannot be measured
|
|
108
|
-
// is rejected: a `NaN` getting through becomes a `NaN` bucket key and a `NaN` `sumMs`
|
|
109
|
-
// for the rest of the session.
|
|
110
|
-
if (!Number.isFinite(entry.duration) || entry.duration < 0) {
|
|
111
|
-
return undefined;
|
|
112
|
-
}
|
|
113
166
|
var tracked = this.interactions.get(interactionId);
|
|
114
167
|
if (tracked === undefined) {
|
|
115
168
|
// Taken once: an entry that only makes the interaction slower has to move its count
|
|
116
169
|
// within the group it was counted in, not into another one.
|
|
117
170
|
var group = this.groupByEvent.get(eventKey(entry.name, entry.startTime));
|
|
118
|
-
|
|
171
|
+
var interaction = {
|
|
172
|
+
group: group,
|
|
119
173
|
latencyMs: entry.duration,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.prune(this.interactions);
|
|
123
|
-
return {
|
|
124
|
-
type: 'new',
|
|
125
|
-
latencyMs: entry.duration,
|
|
126
|
-
group: group
|
|
174
|
+
presentedIn: paint,
|
|
175
|
+
startedAt: entry.startTime
|
|
127
176
|
};
|
|
177
|
+
this.interactions.set(interactionId, interaction);
|
|
178
|
+
return [this.newUpdate(interactionId, interaction)].concat((0, _toConsumableArray2.default)(remeasuredOthers));
|
|
128
179
|
}
|
|
129
|
-
if (entry.duration
|
|
130
|
-
|
|
180
|
+
if (entry.duration > tracked.latencyMs) {
|
|
181
|
+
var previousLatencyMs = tracked.latencyMs;
|
|
182
|
+
tracked.latencyMs = entry.duration;
|
|
183
|
+
tracked.presentedIn = paint;
|
|
184
|
+
tracked.startedAt = entry.startTime;
|
|
185
|
+
return [this.remeasuredUpdate(interactionId, tracked, previousLatencyMs)].concat((0, _toConsumableArray2.default)(remeasuredOthers));
|
|
131
186
|
}
|
|
132
|
-
var fromMs = tracked.latencyMs;
|
|
133
|
-
tracked.latencyMs = entry.duration;
|
|
134
|
-
return {
|
|
135
|
-
type: 'remeasured',
|
|
136
|
-
fromMs: fromMs,
|
|
137
|
-
toMs: entry.duration,
|
|
138
|
-
group: tracked.group
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
187
|
|
|
142
|
-
|
|
188
|
+
// Not the slowest entry of the interaction, so its latency stands. Its handlers still ran
|
|
189
|
+
// before the same paint, if this is that paint, and so moved where that latency went.
|
|
190
|
+
if (!(placement !== null && placement !== void 0 && placement.grew) || paint !== tracked.presentedIn) {
|
|
191
|
+
return remeasuredOthers;
|
|
192
|
+
}
|
|
193
|
+
return [this.remeasuredUpdate(interactionId, tracked, tracked.latencyMs)].concat((0, _toConsumableArray2.default)(remeasuredOthers));
|
|
194
|
+
}
|
|
143
195
|
}, {
|
|
144
196
|
key: "recordEditorEvent",
|
|
145
197
|
value: function recordEditorEvent(event) {
|
|
@@ -151,35 +203,125 @@ var InteractionTracker = exports.InteractionTracker = /*#__PURE__*/function () {
|
|
|
151
203
|
// Every event of the interaction, because any of them can be the one Event Timing reports
|
|
152
204
|
// as the slowest: for a pointer press that is usually the click.
|
|
153
205
|
this.groupByEvent.set(eventKey(event.type, event.timeStamp), kind.group);
|
|
154
|
-
this.prune(this.groupByEvent);
|
|
155
206
|
return kind.counts ? kind.group : undefined;
|
|
156
207
|
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Every interaction whose boundaries are read from this paint, reported as measured again at the
|
|
211
|
+
* latency it already had.
|
|
212
|
+
*
|
|
213
|
+
* @param except the interaction the entry measured, which the caller reports itself. `0` or
|
|
214
|
+
* nothing when the entry measured none, and then no interaction is left out.
|
|
215
|
+
*/
|
|
216
|
+
}, {
|
|
217
|
+
key: "remeasuredUpdatesIn",
|
|
218
|
+
value: function remeasuredUpdatesIn(paint, _ref) {
|
|
219
|
+
var _this = this;
|
|
220
|
+
var except = _ref.except;
|
|
221
|
+
var remeasured = [];
|
|
222
|
+
this.interactions.forEach(function (interaction, interactionId) {
|
|
223
|
+
if (interaction.presentedIn === paint && interactionId !== except) {
|
|
224
|
+
remeasured.push(_this.remeasuredUpdate(interactionId, interaction, interaction.latencyMs));
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
return remeasured;
|
|
228
|
+
}
|
|
229
|
+
}, {
|
|
230
|
+
key: "newUpdate",
|
|
231
|
+
value: function newUpdate(interactionId, tracked) {
|
|
232
|
+
return {
|
|
233
|
+
type: 'new',
|
|
234
|
+
interactionId: interactionId,
|
|
235
|
+
latencyMs: tracked.latencyMs,
|
|
236
|
+
group: tracked.group,
|
|
237
|
+
boundaries: this.boundariesOf(tracked)
|
|
238
|
+
};
|
|
239
|
+
}
|
|
157
240
|
}, {
|
|
158
|
-
key: "
|
|
159
|
-
value: function
|
|
160
|
-
|
|
161
|
-
|
|
241
|
+
key: "remeasuredUpdate",
|
|
242
|
+
value: function remeasuredUpdate(interactionId, tracked, previousLatencyMs) {
|
|
243
|
+
return {
|
|
244
|
+
type: 'remeasured',
|
|
245
|
+
interactionId: interactionId,
|
|
246
|
+
previousLatencyMs: previousLatencyMs,
|
|
247
|
+
latencyMs: tracked.latencyMs,
|
|
248
|
+
group: tracked.group,
|
|
249
|
+
boundaries: this.boundariesOf(tracked)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The four moments of an interaction, read from the paint as it stands now.
|
|
255
|
+
*
|
|
256
|
+
* Limited the way `web-vitals` limits its INP attribution, so the four stay in order: the paint's
|
|
257
|
+
* handlers can have started before the event arrived, and can have finished after the paint the
|
|
258
|
+
* event's rounded-down `duration` points at.
|
|
259
|
+
*
|
|
260
|
+
* @returns nothing when the browser reported no processing timestamps for the interaction, which
|
|
261
|
+
* leaves it in no paint.
|
|
262
|
+
*/
|
|
263
|
+
}, {
|
|
264
|
+
key: "boundariesOf",
|
|
265
|
+
value: function boundariesOf(_ref2) {
|
|
266
|
+
var latencyMs = _ref2.latencyMs,
|
|
267
|
+
presentedIn = _ref2.presentedIn,
|
|
268
|
+
startedAt = _ref2.startedAt;
|
|
269
|
+
if (!presentedIn) {
|
|
270
|
+
return undefined;
|
|
162
271
|
}
|
|
272
|
+
var processingStartedAt = Math.max(presentedIn.processingStartedAt, startedAt);
|
|
273
|
+
var presentedAt = Math.max(startedAt + latencyMs, processingStartedAt);
|
|
274
|
+
var processingEndedAt = Math.min(presentedIn.processingEndedAt, presentedAt);
|
|
275
|
+
return {
|
|
276
|
+
startedAt: startedAt,
|
|
277
|
+
processingStartedAt: processingStartedAt,
|
|
278
|
+
processingEndedAt: processingEndedAt,
|
|
279
|
+
presentedAt: presentedAt
|
|
280
|
+
};
|
|
281
|
+
}
|
|
163
282
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
283
|
+
/**
|
|
284
|
+
* The paint that presented this entry, grown to cover this entry's own processing.
|
|
285
|
+
*
|
|
286
|
+
* The moment being matched is always the one the first entry of the paint reported, so that a
|
|
287
|
+
* run of entries 8 ms apart cannot walk one paint across the next.
|
|
288
|
+
*
|
|
289
|
+
* @returns nothing when the browser reported no processing timestamps for the entry, which
|
|
290
|
+
* leaves nothing to place it by.
|
|
291
|
+
*/
|
|
292
|
+
}, {
|
|
293
|
+
key: "paintOf",
|
|
294
|
+
value: function paintOf(entry) {
|
|
295
|
+
var startTime = entry.startTime,
|
|
296
|
+
duration = entry.duration,
|
|
297
|
+
processingStart = entry.processingStart,
|
|
298
|
+
processingEnd = entry.processingEnd;
|
|
299
|
+
if (typeof processingStart !== 'number' || typeof processingEnd !== 'number') {
|
|
300
|
+
return undefined;
|
|
182
301
|
}
|
|
302
|
+
var presentedAt = startTime + duration;
|
|
303
|
+
var knownPaint = this.recentPaints.findLast(function (paint) {
|
|
304
|
+
return Math.abs(presentedAt - paint.presentedAt) <= PRESENTATION_ROUNDING_MS;
|
|
305
|
+
});
|
|
306
|
+
if (knownPaint) {
|
|
307
|
+
var grew = processingStart < knownPaint.processingStartedAt || processingEnd > knownPaint.processingEndedAt;
|
|
308
|
+
knownPaint.processingStartedAt = Math.min(processingStart, knownPaint.processingStartedAt);
|
|
309
|
+
knownPaint.processingEndedAt = Math.max(processingEnd, knownPaint.processingEndedAt);
|
|
310
|
+
return {
|
|
311
|
+
grew: grew,
|
|
312
|
+
paint: knownPaint
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
var newPaint = {
|
|
316
|
+
presentedAt: presentedAt,
|
|
317
|
+
processingStartedAt: processingStart,
|
|
318
|
+
processingEndedAt: processingEnd
|
|
319
|
+
};
|
|
320
|
+
this.recentPaints.push(newPaint);
|
|
321
|
+
return {
|
|
322
|
+
grew: true,
|
|
323
|
+
paint: newPaint
|
|
324
|
+
};
|
|
183
325
|
}
|
|
184
326
|
}]);
|
|
185
327
|
}();
|