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