@atlaskit/editor-plugin-interactivity 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -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 +2 -2
|
@@ -19,14 +19,28 @@ const REPORTED_QUANTILES = [0.9, 0.98];
|
|
|
19
19
|
* whatever its latency was, and everything the event carries — the reported buckets, the
|
|
20
20
|
* count, the sum, the maximum and the percentiles — is derived from that map when a snapshot
|
|
21
21
|
* is taken. Nothing is computed while interactions arrive.
|
|
22
|
+
*
|
|
23
|
+
* The two counters count different populations: `add` takes the interactions Event Timing
|
|
24
|
+
* measured, `countTotal` takes all of them, including the ones below the 16 ms reporting threshold
|
|
25
|
+
* it never delivers. So `totalCount >= observedCount`, and the difference is how many were too
|
|
26
|
+
* fast to be measured.
|
|
22
27
|
*/
|
|
23
28
|
export class InteractionGroup {
|
|
24
29
|
constructor() {
|
|
25
30
|
_defineProperty(this, "countByLatency", new Map());
|
|
31
|
+
_defineProperty(this, "totalCount", 0);
|
|
26
32
|
}
|
|
27
33
|
add(latencyMs) {
|
|
28
34
|
this.increment(latencyMs);
|
|
29
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Counts an interaction towards the group's total, measured or not. `page` has no use for it:
|
|
39
|
+
* `performance.interactionCount` counts the page's interactions.
|
|
40
|
+
*/
|
|
41
|
+
countTotal() {
|
|
42
|
+
this.totalCount += 1;
|
|
43
|
+
}
|
|
30
44
|
remeasure(previousLatencyMs, latencyMs) {
|
|
31
45
|
// Moved rather than counted again: the count belongs to the same interaction.
|
|
32
46
|
this.decrement(previousLatencyMs);
|
|
@@ -35,9 +49,10 @@ export class InteractionGroup {
|
|
|
35
49
|
|
|
36
50
|
/**
|
|
37
51
|
* @param totalCount every interaction of the group, including those below the Event Timing
|
|
38
|
-
* reporting threshold, which
|
|
52
|
+
* reporting threshold. Defaults to what `countTotal` was told, which is where an editor
|
|
53
|
+
* group's total comes from; `page` passes `performance.interactionCount` instead.
|
|
39
54
|
*/
|
|
40
|
-
snapshot(totalCount) {
|
|
55
|
+
snapshot(totalCount = this.totalCount) {
|
|
41
56
|
var _latencies;
|
|
42
57
|
// Ascending, so the reported buckets come out in order and the last latency is the
|
|
43
58
|
// maximum. Sorted once for everything below.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
import { interactionEventKind } from './interaction-events';
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* The Event Timing fields this package reads. `interactionId` is missing from the DOM
|
|
4
6
|
* typings' `PerformanceEventTiming`, so it is declared here as optional, which also makes
|
|
@@ -8,12 +10,13 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
|
8
10
|
/**
|
|
9
11
|
* What an entry did to the interaction it belongs to: either it is the first entry of a new
|
|
10
12
|
* interaction, or it measured an interaction that was already counted as slower than it was
|
|
11
|
-
* known to be.
|
|
13
|
+
* known to be. Both carry the editor group of the interaction, if it is one of the editor's.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
* How many interactions are remembered, so their growth can still be applied
|
|
16
|
-
* interaction arrive within the interaction itself, so
|
|
17
|
+
* How many interactions are remembered, so their growth can still be applied, and how many of
|
|
18
|
+
* the editor's events. Entries of one interaction arrive within the interaction itself, so
|
|
19
|
+
* anything older than the last few hundred is not needed.
|
|
17
20
|
*/
|
|
18
21
|
const MAX_TRACKED = 256;
|
|
19
22
|
/**
|
|
@@ -23,7 +26,17 @@ const MAX_TRACKED = 256;
|
|
|
23
26
|
const PRUNE_BATCH = 64;
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
|
-
*
|
|
29
|
+
* Identifies the event an entry measured: an entry's `startTime` is that event's timestamp and its
|
|
30
|
+
* `name` is its type. The type is in the key because browsers coarsen the timestamp, so two events
|
|
31
|
+
* of one task can share it — and events of one type are always of one group, so a collision cannot
|
|
32
|
+
* move an interaction into another group.
|
|
33
|
+
*/
|
|
34
|
+
function eventKey(type, timeStamp) {
|
|
35
|
+
return `${type}|${timeStamp}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Makes interactions out of what the two observers report, for one session.
|
|
27
40
|
*
|
|
28
41
|
* Entries sharing a non-zero `interactionId` are one interaction whose latency is the
|
|
29
42
|
* maximum `duration` among them. Entries arrive incrementally, so an interaction's latency
|
|
@@ -33,6 +46,9 @@ const PRUNE_BATCH = 64;
|
|
|
33
46
|
* A non-zero `interactionId` is the browser's own definition of an interaction, which is
|
|
34
47
|
* also what INP filters on: it is assigned to the pointer and keyboard events that make one
|
|
35
48
|
* up, and never to scrolling or pointer movement.
|
|
49
|
+
*
|
|
50
|
+
* The editor's events answer what an entry cannot: which interactions were with the editor, and
|
|
51
|
+
* how many there were, including the ones below the Event Timing reporting threshold.
|
|
36
52
|
*/
|
|
37
53
|
export class InteractionTracker {
|
|
38
54
|
/**
|
|
@@ -43,7 +59,8 @@ export class InteractionTracker {
|
|
|
43
59
|
* be counted in both.
|
|
44
60
|
*/
|
|
45
61
|
constructor(startsAfterInteractionId = 0) {
|
|
46
|
-
_defineProperty(this, "
|
|
62
|
+
_defineProperty(this, "interactions", new Map());
|
|
63
|
+
_defineProperty(this, "groupByEvent", new Map());
|
|
47
64
|
_defineProperty(this, "highestInteractionId", 0);
|
|
48
65
|
this.startsAfterInteractionId = startsAfterInteractionId;
|
|
49
66
|
}
|
|
@@ -78,35 +95,58 @@ export class InteractionTracker {
|
|
|
78
95
|
if (!Number.isFinite(entry.duration) || entry.duration < 0) {
|
|
79
96
|
return undefined;
|
|
80
97
|
}
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
const tracked = this.interactions.get(interactionId);
|
|
99
|
+
if (tracked === undefined) {
|
|
100
|
+
// Taken once: an entry that only makes the interaction slower has to move its count
|
|
101
|
+
// within the group it was counted in, not into another one.
|
|
102
|
+
const group = this.groupByEvent.get(eventKey(entry.name, entry.startTime));
|
|
103
|
+
this.interactions.set(interactionId, {
|
|
104
|
+
latencyMs: entry.duration,
|
|
105
|
+
group
|
|
106
|
+
});
|
|
107
|
+
this.prune(this.interactions);
|
|
85
108
|
return {
|
|
86
109
|
type: 'new',
|
|
87
|
-
latencyMs: entry.duration
|
|
110
|
+
latencyMs: entry.duration,
|
|
111
|
+
group
|
|
88
112
|
};
|
|
89
113
|
}
|
|
90
|
-
if (entry.duration <=
|
|
114
|
+
if (entry.duration <= tracked.latencyMs) {
|
|
91
115
|
return undefined;
|
|
92
116
|
}
|
|
93
|
-
|
|
117
|
+
const fromMs = tracked.latencyMs;
|
|
118
|
+
tracked.latencyMs = entry.duration;
|
|
94
119
|
return {
|
|
95
120
|
type: 'remeasured',
|
|
96
|
-
fromMs
|
|
97
|
-
toMs: entry.duration
|
|
121
|
+
fromMs,
|
|
122
|
+
toMs: entry.duration,
|
|
123
|
+
group: tracked.group
|
|
98
124
|
};
|
|
99
125
|
}
|
|
100
|
-
|
|
101
|
-
|
|
126
|
+
|
|
127
|
+
/** @returns the group of an interaction to count, when this is the event its group counts on. */
|
|
128
|
+
recordEditorEvent(event) {
|
|
129
|
+
const kind = interactionEventKind(event.type);
|
|
130
|
+
if (!kind) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Every event of the interaction, because any of them can be the one Event Timing reports
|
|
135
|
+
// as the slowest: for a pointer press that is usually the click.
|
|
136
|
+
this.groupByEvent.set(eventKey(event.type, event.timeStamp), kind.group);
|
|
137
|
+
this.prune(this.groupByEvent);
|
|
138
|
+
return kind.counts ? kind.group : undefined;
|
|
139
|
+
}
|
|
140
|
+
prune(entries) {
|
|
141
|
+
if (entries.size <= MAX_TRACKED) {
|
|
102
142
|
return;
|
|
103
143
|
}
|
|
104
144
|
|
|
105
|
-
// `Map` preserves insertion order
|
|
106
|
-
//
|
|
145
|
+
// `Map` preserves insertion order, so the entries inserted first are the least likely to
|
|
146
|
+
// see another entry or another event.
|
|
107
147
|
let remaining = PRUNE_BATCH;
|
|
108
|
-
for (const
|
|
109
|
-
|
|
148
|
+
for (const key of entries.keys()) {
|
|
149
|
+
entries.delete(key);
|
|
110
150
|
remaining -= 1;
|
|
111
151
|
if (remaining === 0) {
|
|
112
152
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
2
|
import { getDocument } from '@atlaskit/browser-apis';
|
|
3
|
+
import { EditorEventObserver } from './editor-event-observer';
|
|
3
4
|
import { InteractionObserver } from './interaction-observer';
|
|
4
5
|
import { InteractivitySession } from './interactivity-session';
|
|
5
6
|
import { SCHEMA_VERSION } from './bucket-boundaries';
|
|
@@ -8,6 +9,9 @@ import { SnapshotScheduler } from './snapshot-scheduler';
|
|
|
8
9
|
/**
|
|
9
10
|
* Collects interaction latencies for one editor mount and emits session-to-date snapshots.
|
|
10
11
|
*
|
|
12
|
+
* Every interaction is counted in `page`, and the ones with the editor again in one of the editor
|
|
13
|
+
* groups, which is what tells a slow editor apart from a slow page around it.
|
|
14
|
+
*
|
|
11
15
|
* `start` and `stop` bound the collecting, which happens once. Within it there can be several
|
|
12
16
|
* sessions, because a session covers one document in one mode: a Confluence live page
|
|
13
17
|
* navigates and switches between reading and editing without ever remounting the editor, and
|
|
@@ -23,6 +27,7 @@ export class InteractivityCollector {
|
|
|
23
27
|
this.getObjectId = options.getObjectId;
|
|
24
28
|
this.getSessionMode = options.getSessionMode;
|
|
25
29
|
this.interactionObserver = new InteractionObserver(entries => this.recordEntries(entries));
|
|
30
|
+
this.editorEvents = new EditorEventObserver(event => this.recordEditorEvent(event));
|
|
26
31
|
this.snapshotScheduler = new SnapshotScheduler(() => this.onTimer());
|
|
27
32
|
this.lifecycleObserver = new LifecycleObserver({
|
|
28
33
|
onHidden: () => this.onHidden(),
|
|
@@ -55,6 +60,7 @@ export class InteractivityCollector {
|
|
|
55
60
|
// this object was constructed.
|
|
56
61
|
this.session = this.createSession(0);
|
|
57
62
|
this.interactionObserver.start();
|
|
63
|
+
this.editorEvents.observe(this.editorRoot);
|
|
58
64
|
this.snapshotScheduler.start();
|
|
59
65
|
this.lifecycleObserver.start();
|
|
60
66
|
this.started = true;
|
|
@@ -69,10 +75,22 @@ export class InteractivityCollector {
|
|
|
69
75
|
this.takeSnapshot('unmount');
|
|
70
76
|
this.stopped = true;
|
|
71
77
|
this.interactionObserver.stop();
|
|
78
|
+
this.editorEvents.stop();
|
|
72
79
|
this.snapshotScheduler.stop();
|
|
73
80
|
this.lifecycleObserver.stop();
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The element the editor renders itself into is what makes an interaction one of the editor's.
|
|
85
|
+
* The session is not tied to it: interactions from before it arrives are counted in `page`.
|
|
86
|
+
*/
|
|
87
|
+
setEditorRoot(root) {
|
|
88
|
+
this.editorRoot = root !== null && root !== void 0 ? root : undefined;
|
|
89
|
+
if (this.started && !this.stopped) {
|
|
90
|
+
this.editorEvents.observe(this.editorRoot);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
76
94
|
/**
|
|
77
95
|
* Rotates the session when the editor is pointed at different content.
|
|
78
96
|
*
|
|
@@ -164,6 +182,24 @@ export class InteractivityCollector {
|
|
|
164
182
|
onPageShow() {
|
|
165
183
|
this.session.lifecycleSnapshotEmitted = false;
|
|
166
184
|
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Counts an interaction with the editor, including the ones Event Timing never reports because
|
|
188
|
+
* they were faster than its threshold — which is why a count alone moves the session on.
|
|
189
|
+
*/
|
|
190
|
+
recordEditorEvent(event) {
|
|
191
|
+
if (this.stopped) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const group = this.session.tracker.recordEditorEvent(event);
|
|
195
|
+
if (!group) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The session names its editor groups after the fields they are reported in.
|
|
200
|
+
this.session[group].countTotal();
|
|
201
|
+
this.session.revision += 1;
|
|
202
|
+
}
|
|
167
203
|
recordEntries(entries) {
|
|
168
204
|
const {
|
|
169
205
|
tracker,
|
|
@@ -176,8 +212,14 @@ export class InteractivityCollector {
|
|
|
176
212
|
}
|
|
177
213
|
if (update.type === 'new') {
|
|
178
214
|
page.add(update.latencyMs);
|
|
215
|
+
if (update.group) {
|
|
216
|
+
this.session[update.group].add(update.latencyMs);
|
|
217
|
+
}
|
|
179
218
|
} else {
|
|
180
219
|
page.remeasure(update.fromMs, update.toMs);
|
|
220
|
+
if (update.group) {
|
|
221
|
+
this.session[update.group].remeasure(update.fromMs, update.toMs);
|
|
222
|
+
}
|
|
181
223
|
}
|
|
182
224
|
this.session.revision += 1;
|
|
183
225
|
}
|
|
@@ -210,7 +252,7 @@ export class InteractivityCollector {
|
|
|
210
252
|
session.emittedRevision = session.revision;
|
|
211
253
|
const now = performance.now();
|
|
212
254
|
const hiddenMs = session.hiddenMs + (session.hiddenSince === undefined ? 0 : now - session.hiddenSince);
|
|
213
|
-
const
|
|
255
|
+
const pageTotalCount = InteractionObserver.readPageInteractionCount() - session.interactionCountAtStart;
|
|
214
256
|
this.emit({
|
|
215
257
|
schema: SCHEMA_VERSION,
|
|
216
258
|
interactivitySessionId: session.id,
|
|
@@ -222,7 +264,11 @@ export class InteractivityCollector {
|
|
|
222
264
|
hiddenMs: Math.round(hiddenMs),
|
|
223
265
|
nodeSize: this.getNodeSize(),
|
|
224
266
|
editorDomSize: this.getEditorDomSize(),
|
|
225
|
-
page
|
|
267
|
+
// Only `page` is told its total; each editor group has counted its own.
|
|
268
|
+
page: session.page.snapshot(pageTotalCount),
|
|
269
|
+
editorTyping: session.editorTyping.snapshot(),
|
|
270
|
+
editorPointer: session.editorPointer.snapshot(),
|
|
271
|
+
editorOther: session.editorOther.snapshot()
|
|
226
272
|
});
|
|
227
273
|
}
|
|
228
274
|
}
|
|
@@ -23,6 +23,9 @@ export class InteractivitySession {
|
|
|
23
23
|
/** Page interaction count when the session opened, subtracted to get its own total. */
|
|
24
24
|
_defineProperty(this, "interactionCountAtStart", InteractionObserver.readPageInteractionCount());
|
|
25
25
|
_defineProperty(this, "page", new InteractionGroup());
|
|
26
|
+
_defineProperty(this, "editorTyping", new InteractionGroup());
|
|
27
|
+
_defineProperty(this, "editorPointer", new InteractionGroup());
|
|
28
|
+
_defineProperty(this, "editorOther", new InteractionGroup());
|
|
26
29
|
/** Increments per snapshot; a query takes the highest one per session. */
|
|
27
30
|
_defineProperty(this, "seq", 0);
|
|
28
31
|
_defineProperty(this, "hiddenMs", 0);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { logException } from '@atlaskit/editor-common/monitoring';
|
|
3
3
|
import { fireInteractivityEvent } from './analytics/fire-interactivity-event';
|
|
4
4
|
import { InteractivityCollector } from './collector/interactivity-collector';
|
|
@@ -16,12 +16,11 @@ export const interactivityPlugin = ({
|
|
|
16
16
|
}) => ({
|
|
17
17
|
name: 'interactivity',
|
|
18
18
|
usePluginHook({
|
|
19
|
-
editorView
|
|
19
|
+
editorView,
|
|
20
|
+
wrapperElement
|
|
20
21
|
}) {
|
|
22
|
+
const collectorRef = useRef(undefined);
|
|
21
23
|
useEffect(() => {
|
|
22
|
-
// Nothing measured here is worth an editor. This effect runs inside the plugin slot's
|
|
23
|
-
// error boundary, which also renders every plugin's content components, so a throw
|
|
24
|
-
// from instrumentation would take that UI down with it.
|
|
25
24
|
try {
|
|
26
25
|
var _api$contextIdentifie4, _api$editorViewMode2;
|
|
27
26
|
const collector = new InteractivityCollector({
|
|
@@ -48,6 +47,9 @@ export const interactivityPlugin = ({
|
|
|
48
47
|
// cannot overflow the stack on very large documents.
|
|
49
48
|
getEditorDomSize: () => editorView.isDestroyed ? undefined : editorView.dom.getElementsByTagName('*').length
|
|
50
49
|
});
|
|
50
|
+
|
|
51
|
+
// Effects run in declaration order, so the one below always finds it.
|
|
52
|
+
collectorRef.current = collector;
|
|
51
53
|
if (!collector.start()) {
|
|
52
54
|
return;
|
|
53
55
|
}
|
|
@@ -75,5 +77,15 @@ export const interactivityPlugin = ({
|
|
|
75
77
|
// live registry, and restarting the session on a reconfigure would split one
|
|
76
78
|
// editor session in two.
|
|
77
79
|
}, [editorView]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
try {
|
|
82
|
+
var _collectorRef$current;
|
|
83
|
+
(_collectorRef$current = collectorRef.current) === null || _collectorRef$current === void 0 ? void 0 : _collectorRef$current.setEditorRoot(wrapperElement);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
void logException(error, {
|
|
86
|
+
location: 'editor-plugin-interactivity/observe'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}, [wrapperElement]);
|
|
78
90
|
}
|
|
79
91
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
|
|
2
|
+
import _createClass from "@babel/runtime/helpers/createClass";
|
|
3
|
+
import { bindAll } from 'bind-event-listener';
|
|
4
|
+
import { INTERACTION_EVENT_TYPES } from './interaction-events';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reports the events the interactions with the editor are made of, as the browser dispatched
|
|
8
|
+
* them. What they mean is the tracker's business.
|
|
9
|
+
*
|
|
10
|
+
* Event Timing cannot answer which interactions were with the editor, because it observes the
|
|
11
|
+
* whole document, nor how many there were, because it reports none below 16 ms.
|
|
12
|
+
*/
|
|
13
|
+
export var EditorEventObserver = /*#__PURE__*/function () {
|
|
14
|
+
function EditorEventObserver(onEvent) {
|
|
15
|
+
_classCallCheck(this, EditorEventObserver);
|
|
16
|
+
this.onEvent = onEvent;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Called as the element the editor renders itself into changes: the editor only knows it after
|
|
21
|
+
* its first render, and can replace it.
|
|
22
|
+
*/
|
|
23
|
+
return _createClass(EditorEventObserver, [{
|
|
24
|
+
key: "observe",
|
|
25
|
+
value: function observe(root) {
|
|
26
|
+
var _this = this;
|
|
27
|
+
var next = root !== null && root !== void 0 ? root : undefined;
|
|
28
|
+
if (next === this.root) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.stop();
|
|
32
|
+
this.root = next;
|
|
33
|
+
if (!next) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.unbind = bindAll(next, INTERACTION_EVENT_TYPES.map(function (type) {
|
|
37
|
+
return {
|
|
38
|
+
type: type,
|
|
39
|
+
listener: _this.onEvent
|
|
40
|
+
};
|
|
41
|
+
}),
|
|
42
|
+
// Capture, so a handler in the editor cannot stop an interaction from being reported.
|
|
43
|
+
{
|
|
44
|
+
capture: true,
|
|
45
|
+
passive: true
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}, {
|
|
49
|
+
key: "stop",
|
|
50
|
+
value: function stop() {
|
|
51
|
+
var _this$unbind;
|
|
52
|
+
(_this$unbind = this.unbind) === null || _this$unbind === void 0 || _this$unbind.call(this);
|
|
53
|
+
this.unbind = undefined;
|
|
54
|
+
this.root = undefined;
|
|
55
|
+
}
|
|
56
|
+
}]);
|
|
57
|
+
}();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor group an interaction belongs to, which is also the field the event reports it in.
|
|
3
|
+
*
|
|
4
|
+
* `editorOther` is the remainder — an interaction the browser counts that is neither typing nor
|
|
5
|
+
* pointing — so the three groups together cover whatever the browser calls an interaction.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The events an interaction is made of, and the group they belong to. Both sides of the collector
|
|
10
|
+
* read this, so counting and grouping cannot disagree.
|
|
11
|
+
*
|
|
12
|
+
* These are the events Event Timing can report with a non-zero `interactionId`, so any of them can
|
|
13
|
+
* be the one an entry measured — and it is usually not the one the interaction is counted on: for a
|
|
14
|
+
* pointer press the slow part is normally the `click` handler.
|
|
15
|
+
*
|
|
16
|
+
* The counted event is at a different end of the interaction in each group, because what can go
|
|
17
|
+
* wrong differs. Typing is counted on `keydown`, since the browser opens a new interaction for
|
|
18
|
+
* every repeat of a held key, and nothing cancels a key press. Pointing is counted on `pointerup`,
|
|
19
|
+
* since a press cannot repeat but can be taken over by a scroll — which the browser does not count
|
|
20
|
+
* either, and in that case `pointerup` never arrives.
|
|
21
|
+
*/
|
|
22
|
+
var INTERACTION_EVENTS = {
|
|
23
|
+
keydown: {
|
|
24
|
+
group: 'editorTyping',
|
|
25
|
+
counts: true
|
|
26
|
+
},
|
|
27
|
+
keyup: {
|
|
28
|
+
group: 'editorTyping'
|
|
29
|
+
},
|
|
30
|
+
// Only while an IME composes: the browser counts the text it commits in the interaction of the
|
|
31
|
+
// key that caused it.
|
|
32
|
+
input: {
|
|
33
|
+
group: 'editorTyping'
|
|
34
|
+
},
|
|
35
|
+
pointerdown: {
|
|
36
|
+
group: 'editorPointer'
|
|
37
|
+
},
|
|
38
|
+
pointerup: {
|
|
39
|
+
group: 'editorPointer',
|
|
40
|
+
counts: true
|
|
41
|
+
},
|
|
42
|
+
click: {
|
|
43
|
+
group: 'editorPointer'
|
|
44
|
+
},
|
|
45
|
+
// Ends a pointer press the way `pointerup` does, so the browser counts it as an interaction.
|
|
46
|
+
contextmenu: {
|
|
47
|
+
group: 'editorPointer'
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Derived, so the types listened for cannot drift from the table. */
|
|
52
|
+
export var INTERACTION_EVENT_TYPES = Object.keys(INTERACTION_EVENTS);
|
|
53
|
+
export function interactionEventKind(type) {
|
|
54
|
+
return INTERACTION_EVENTS[type];
|
|
55
|
+
}
|
|
@@ -24,17 +24,33 @@ var REPORTED_QUANTILES = [0.9, 0.98];
|
|
|
24
24
|
* whatever its latency was, and everything the event carries — the reported buckets, the
|
|
25
25
|
* count, the sum, the maximum and the percentiles — is derived from that map when a snapshot
|
|
26
26
|
* is taken. Nothing is computed while interactions arrive.
|
|
27
|
+
*
|
|
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
32
|
*/
|
|
28
33
|
export var InteractionGroup = /*#__PURE__*/function () {
|
|
29
34
|
function InteractionGroup() {
|
|
30
35
|
_classCallCheck(this, InteractionGroup);
|
|
31
36
|
_defineProperty(this, "countByLatency", new Map());
|
|
37
|
+
_defineProperty(this, "totalCount", 0);
|
|
32
38
|
}
|
|
33
39
|
return _createClass(InteractionGroup, [{
|
|
34
40
|
key: "add",
|
|
35
41
|
value: function add(latencyMs) {
|
|
36
42
|
this.increment(latencyMs);
|
|
37
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Counts an interaction towards the group's total, measured or not. `page` has no use for it:
|
|
47
|
+
* `performance.interactionCount` counts the page's interactions.
|
|
48
|
+
*/
|
|
49
|
+
}, {
|
|
50
|
+
key: "countTotal",
|
|
51
|
+
value: function countTotal() {
|
|
52
|
+
this.totalCount += 1;
|
|
53
|
+
}
|
|
38
54
|
}, {
|
|
39
55
|
key: "remeasure",
|
|
40
56
|
value: function remeasure(previousLatencyMs, latencyMs) {
|
|
@@ -45,12 +61,14 @@ export var InteractionGroup = /*#__PURE__*/function () {
|
|
|
45
61
|
|
|
46
62
|
/**
|
|
47
63
|
* @param totalCount every interaction of the group, including those below the Event Timing
|
|
48
|
-
* reporting threshold, which
|
|
64
|
+
* reporting threshold. Defaults to what `countTotal` was told, which is where an editor
|
|
65
|
+
* group's total comes from; `page` passes `performance.interactionCount` instead.
|
|
49
66
|
*/
|
|
50
67
|
}, {
|
|
51
68
|
key: "snapshot",
|
|
52
|
-
value: function snapshot(
|
|
69
|
+
value: function snapshot() {
|
|
53
70
|
var _latencies;
|
|
71
|
+
var totalCount = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.totalCount;
|
|
54
72
|
// Ascending, so the reported buckets come out in order and the last latency is the
|
|
55
73
|
// maximum. Sorted once for everything below.
|
|
56
74
|
var latencies = Array.from(this.countByLatency.keys()).sort(function (a, b) {
|
|
@@ -4,6 +4,8 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
|
4
4
|
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
|
|
5
5
|
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
6
6
|
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
7
|
+
import { interactionEventKind } from './interaction-events';
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* The Event Timing fields this package reads. `interactionId` is missing from the DOM
|
|
9
11
|
* typings' `PerformanceEventTiming`, so it is declared here as optional, which also makes
|
|
@@ -13,12 +15,13 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
|
|
|
13
15
|
/**
|
|
14
16
|
* What an entry did to the interaction it belongs to: either it is the first entry of a new
|
|
15
17
|
* interaction, or it measured an interaction that was already counted as slower than it was
|
|
16
|
-
* known to be.
|
|
18
|
+
* known to be. Both carry the editor group of the interaction, if it is one of the editor's.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
|
-
* How many interactions are remembered, so their growth can still be applied
|
|
21
|
-
* interaction arrive within the interaction itself, so
|
|
22
|
+
* How many interactions are remembered, so their growth can still be applied, and how many of
|
|
23
|
+
* the editor's events. Entries of one interaction arrive within the interaction itself, so
|
|
24
|
+
* anything older than the last few hundred is not needed.
|
|
22
25
|
*/
|
|
23
26
|
var MAX_TRACKED = 256;
|
|
24
27
|
/**
|
|
@@ -28,7 +31,17 @@ var MAX_TRACKED = 256;
|
|
|
28
31
|
var PRUNE_BATCH = 64;
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
|
-
*
|
|
34
|
+
* Identifies the event an entry measured: an entry's `startTime` is that event's timestamp and its
|
|
35
|
+
* `name` is its type. The type is in the key because browsers coarsen the timestamp, so two events
|
|
36
|
+
* of one task can share it — and events of one type are always of one group, so a collision cannot
|
|
37
|
+
* move an interaction into another group.
|
|
38
|
+
*/
|
|
39
|
+
function eventKey(type, timeStamp) {
|
|
40
|
+
return "".concat(type, "|").concat(timeStamp);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Makes interactions out of what the two observers report, for one session.
|
|
32
45
|
*
|
|
33
46
|
* Entries sharing a non-zero `interactionId` are one interaction whose latency is the
|
|
34
47
|
* maximum `duration` among them. Entries arrive incrementally, so an interaction's latency
|
|
@@ -38,6 +51,9 @@ var PRUNE_BATCH = 64;
|
|
|
38
51
|
* A non-zero `interactionId` is the browser's own definition of an interaction, which is
|
|
39
52
|
* also what INP filters on: it is assigned to the pointer and keyboard events that make one
|
|
40
53
|
* up, and never to scrolling or pointer movement.
|
|
54
|
+
*
|
|
55
|
+
* The editor's events answer what an entry cannot: which interactions were with the editor, and
|
|
56
|
+
* how many there were, including the ones below the Event Timing reporting threshold.
|
|
41
57
|
*/
|
|
42
58
|
export var InteractionTracker = /*#__PURE__*/function () {
|
|
43
59
|
/**
|
|
@@ -50,7 +66,8 @@ export var InteractionTracker = /*#__PURE__*/function () {
|
|
|
50
66
|
function InteractionTracker() {
|
|
51
67
|
var startsAfterInteractionId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
|
|
52
68
|
_classCallCheck(this, InteractionTracker);
|
|
53
|
-
_defineProperty(this, "
|
|
69
|
+
_defineProperty(this, "interactions", new Map());
|
|
70
|
+
_defineProperty(this, "groupByEvent", new Map());
|
|
54
71
|
_defineProperty(this, "highestInteractionId", 0);
|
|
55
72
|
this.startsAfterInteractionId = startsAfterInteractionId;
|
|
56
73
|
}
|
|
@@ -87,41 +104,66 @@ export var InteractionTracker = /*#__PURE__*/function () {
|
|
|
87
104
|
if (!Number.isFinite(entry.duration) || entry.duration < 0) {
|
|
88
105
|
return undefined;
|
|
89
106
|
}
|
|
90
|
-
var
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
var tracked = this.interactions.get(interactionId);
|
|
108
|
+
if (tracked === undefined) {
|
|
109
|
+
// Taken once: an entry that only makes the interaction slower has to move its count
|
|
110
|
+
// within the group it was counted in, not into another one.
|
|
111
|
+
var group = this.groupByEvent.get(eventKey(entry.name, entry.startTime));
|
|
112
|
+
this.interactions.set(interactionId, {
|
|
113
|
+
latencyMs: entry.duration,
|
|
114
|
+
group: group
|
|
115
|
+
});
|
|
116
|
+
this.prune(this.interactions);
|
|
94
117
|
return {
|
|
95
118
|
type: 'new',
|
|
96
|
-
latencyMs: entry.duration
|
|
119
|
+
latencyMs: entry.duration,
|
|
120
|
+
group: group
|
|
97
121
|
};
|
|
98
122
|
}
|
|
99
|
-
if (entry.duration <=
|
|
123
|
+
if (entry.duration <= tracked.latencyMs) {
|
|
100
124
|
return undefined;
|
|
101
125
|
}
|
|
102
|
-
|
|
126
|
+
var fromMs = tracked.latencyMs;
|
|
127
|
+
tracked.latencyMs = entry.duration;
|
|
103
128
|
return {
|
|
104
129
|
type: 'remeasured',
|
|
105
|
-
fromMs:
|
|
106
|
-
toMs: entry.duration
|
|
130
|
+
fromMs: fromMs,
|
|
131
|
+
toMs: entry.duration,
|
|
132
|
+
group: tracked.group
|
|
107
133
|
};
|
|
108
134
|
}
|
|
135
|
+
|
|
136
|
+
/** @returns the group of an interaction to count, when this is the event its group counts on. */
|
|
137
|
+
}, {
|
|
138
|
+
key: "recordEditorEvent",
|
|
139
|
+
value: function recordEditorEvent(event) {
|
|
140
|
+
var kind = interactionEventKind(event.type);
|
|
141
|
+
if (!kind) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Every event of the interaction, because any of them can be the one Event Timing reports
|
|
146
|
+
// as the slowest: for a pointer press that is usually the click.
|
|
147
|
+
this.groupByEvent.set(eventKey(event.type, event.timeStamp), kind.group);
|
|
148
|
+
this.prune(this.groupByEvent);
|
|
149
|
+
return kind.counts ? kind.group : undefined;
|
|
150
|
+
}
|
|
109
151
|
}, {
|
|
110
152
|
key: "prune",
|
|
111
|
-
value: function prune() {
|
|
112
|
-
if (
|
|
153
|
+
value: function prune(entries) {
|
|
154
|
+
if (entries.size <= MAX_TRACKED) {
|
|
113
155
|
return;
|
|
114
156
|
}
|
|
115
157
|
|
|
116
|
-
// `Map` preserves insertion order
|
|
117
|
-
//
|
|
158
|
+
// `Map` preserves insertion order, so the entries inserted first are the least likely to
|
|
159
|
+
// see another entry or another event.
|
|
118
160
|
var remaining = PRUNE_BATCH;
|
|
119
|
-
var _iterator = _createForOfIteratorHelper(
|
|
161
|
+
var _iterator = _createForOfIteratorHelper(entries.keys()),
|
|
120
162
|
_step;
|
|
121
163
|
try {
|
|
122
164
|
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
123
|
-
var
|
|
124
|
-
|
|
165
|
+
var key = _step.value;
|
|
166
|
+
entries.delete(key);
|
|
125
167
|
remaining -= 1;
|
|
126
168
|
if (remaining === 0) {
|
|
127
169
|
return;
|