@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.
Files changed (32) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +31 -0
  3. package/dist/cjs/collector/editor-event-observer.js +63 -0
  4. package/dist/cjs/collector/interaction-events.js +62 -0
  5. package/dist/cjs/collector/interaction-group.js +20 -2
  6. package/dist/cjs/collector/interaction-tracker.js +62 -21
  7. package/dist/cjs/collector/interactivity-collector.js +54 -2
  8. package/dist/cjs/collector/interactivity-session.js +3 -0
  9. package/dist/cjs/interactivityPlugin.js +16 -4
  10. package/dist/es2019/collector/editor-event-observer.js +46 -0
  11. package/dist/es2019/collector/interaction-events.js +55 -0
  12. package/dist/es2019/collector/interaction-group.js +17 -2
  13. package/dist/es2019/collector/interaction-tracker.js +60 -20
  14. package/dist/es2019/collector/interactivity-collector.js +48 -2
  15. package/dist/es2019/collector/interactivity-session.js +3 -0
  16. package/dist/es2019/interactivityPlugin.js +17 -5
  17. package/dist/esm/collector/editor-event-observer.js +57 -0
  18. package/dist/esm/collector/interaction-events.js +55 -0
  19. package/dist/esm/collector/interaction-group.js +20 -2
  20. package/dist/esm/collector/interaction-tracker.js +63 -21
  21. package/dist/esm/collector/interactivity-collector.js +54 -2
  22. package/dist/esm/collector/interactivity-session.js +3 -0
  23. package/dist/esm/interactivityPlugin.js +17 -5
  24. package/dist/types/analytics/interactivity-snapshot.d.ts +5 -0
  25. package/dist/types/collector/editor-event-observer.d.ts +19 -0
  26. package/dist/types/collector/interaction-events.d.ts +19 -0
  27. package/dist/types/collector/interaction-group.d.ts +14 -2
  28. package/dist/types/collector/interaction-tracker.d.ts +12 -3
  29. package/dist/types/collector/interactivity-collector.d.ts +15 -0
  30. package/dist/types/collector/interactivity-session.d.ts +3 -0
  31. package/docs/0-intro.tsx +2 -1
  32. package/package.json +5 -5
@@ -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
+ const 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 const INTERACTION_EVENT_TYPES = Object.keys(INTERACTION_EVENTS);
53
+ export function interactionEventKind(type) {
54
+ return INTERACTION_EVENTS[type];
55
+ }
@@ -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 this group never sees.
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. Entries of one
16
- * interaction arrive within the interaction itself, so anything older is not needed.
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
- * Groups Event Timing entries into interactions.
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, "latencies", new Map());
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 previousLatencyMs = this.latencies.get(interactionId);
82
- if (previousLatencyMs === undefined) {
83
- this.latencies.set(interactionId, entry.duration);
84
- this.prune();
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 <= previousLatencyMs) {
114
+ if (entry.duration <= tracked.latencyMs) {
91
115
  return undefined;
92
116
  }
93
- this.latencies.set(interactionId, entry.duration);
117
+ const fromMs = tracked.latencyMs;
118
+ tracked.latencyMs = entry.duration;
94
119
  return {
95
120
  type: 'remeasured',
96
- fromMs: previousLatencyMs,
97
- toMs: entry.duration
121
+ fromMs,
122
+ toMs: entry.duration,
123
+ group: tracked.group
98
124
  };
99
125
  }
100
- prune() {
101
- if (this.latencies.size <= MAX_TRACKED) {
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 and `interactionId` increases monotonically,
106
- // so the entries inserted first are the least likely to see another entry.
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 interactionId of this.latencies.keys()) {
109
- this.latencies.delete(interactionId);
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 totalCount = InteractionObserver.readPageInteractionCount() - session.interactionCountAtStart;
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: session.page.snapshot(totalCount)
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 this group never sees.
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(totalCount) {
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) {