@atlaskit/editor-plugin-interactivity 0.1.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +65 -6
  3. package/afm-cc/tsconfig.json +12 -0
  4. package/afm-products/tsconfig.json +12 -0
  5. package/dist/cjs/analytics/fire-interactivity-event.js +39 -0
  6. package/dist/cjs/analytics/interactivity-snapshot.js +1 -0
  7. package/dist/cjs/collector/bucket-boundaries.js +137 -0
  8. package/dist/cjs/collector/editor-event-observer.js +63 -0
  9. package/dist/cjs/collector/interaction-events.js +62 -0
  10. package/dist/cjs/collector/interaction-group.js +185 -0
  11. package/dist/cjs/collector/interaction-observer.js +114 -0
  12. package/dist/cjs/collector/interaction-tracker.js +185 -0
  13. package/dist/cjs/collector/interactivity-collector.js +341 -0
  14. package/dist/cjs/collector/interactivity-session.js +55 -0
  15. package/dist/cjs/collector/lifecycle-observer.js +66 -0
  16. package/dist/cjs/collector/snapshot-scheduler.js +70 -0
  17. package/dist/cjs/interactivityPlugin.js +93 -6
  18. package/dist/es2019/analytics/fire-interactivity-event.js +33 -0
  19. package/dist/es2019/analytics/interactivity-snapshot.js +0 -0
  20. package/dist/es2019/collector/bucket-boundaries.js +117 -0
  21. package/dist/es2019/collector/editor-event-observer.js +46 -0
  22. package/dist/es2019/collector/interaction-events.js +55 -0
  23. package/dist/es2019/collector/interaction-group.js +125 -0
  24. package/dist/es2019/collector/interaction-observer.js +90 -0
  25. package/dist/es2019/collector/interaction-tracker.js +156 -0
  26. package/dist/es2019/collector/interactivity-collector.js +274 -0
  27. package/dist/es2019/collector/interactivity-session.js +45 -0
  28. package/dist/es2019/collector/lifecycle-observer.js +46 -0
  29. package/dist/es2019/collector/snapshot-scheduler.js +48 -0
  30. package/dist/es2019/interactivityPlugin.js +87 -6
  31. package/dist/esm/analytics/fire-interactivity-event.js +33 -0
  32. package/dist/esm/analytics/interactivity-snapshot.js +0 -0
  33. package/dist/esm/collector/bucket-boundaries.js +130 -0
  34. package/dist/esm/collector/editor-event-observer.js +57 -0
  35. package/dist/esm/collector/interaction-events.js +55 -0
  36. package/dist/esm/collector/interaction-group.js +179 -0
  37. package/dist/esm/collector/interaction-observer.js +108 -0
  38. package/dist/esm/collector/interaction-tracker.js +179 -0
  39. package/dist/esm/collector/interactivity-collector.js +334 -0
  40. package/dist/esm/collector/interactivity-session.js +48 -0
  41. package/dist/esm/collector/lifecycle-observer.js +59 -0
  42. package/dist/esm/collector/snapshot-scheduler.js +63 -0
  43. package/dist/esm/interactivityPlugin.js +93 -6
  44. package/dist/types/analytics/fire-interactivity-event.d.ts +9 -0
  45. package/dist/types/analytics/interactivity-snapshot.d.ts +66 -0
  46. package/dist/types/collector/bucket-boundaries.d.ts +35 -0
  47. package/dist/types/collector/editor-event-observer.d.ts +19 -0
  48. package/dist/types/collector/interaction-events.d.ts +19 -0
  49. package/dist/types/collector/interaction-group.d.ts +37 -0
  50. package/dist/types/collector/interaction-observer.d.ts +39 -0
  51. package/dist/types/collector/interaction-tracker.d.ts +65 -0
  52. package/dist/types/collector/interactivity-collector.d.ts +90 -0
  53. package/dist/types/collector/interactivity-session.d.ts +42 -0
  54. package/dist/types/collector/lifecycle-observer.d.ts +25 -0
  55. package/dist/types/collector/snapshot-scheduler.d.ts +15 -0
  56. package/dist/types/interactivityPlugin.d.ts +6 -4
  57. package/dist/types/interactivityPluginType.d.ts +11 -2
  58. package/docs/0-intro.tsx +26 -7
  59. package/package.json +8 -3
@@ -0,0 +1,46 @@
1
+ import { bind } from 'bind-event-listener';
2
+ import { getDocument } from '@atlaskit/browser-apis';
3
+ /**
4
+ * Reports the page lifecycle signals that force an extra snapshot: the tab being hidden or
5
+ * shown again, and the page being unloaded or restored from the back/forward cache.
6
+ *
7
+ * None of them is guaranteed to arrive — a mobile browser being killed raises nothing at all.
8
+ * Snapshots carry session-to-date values, so losing the last one costs only the tail of that
9
+ * session.
10
+ */
11
+ export class LifecycleObserver {
12
+ constructor(handlers) {
13
+ this.handlers = handlers;
14
+ }
15
+ start() {
16
+ const doc = getDocument();
17
+ const unbindVisibilityChange = doc ? bind(doc, {
18
+ type: 'visibilitychange',
19
+ listener: () => {
20
+ if (doc.visibilityState === 'hidden') {
21
+ this.handlers.onHidden();
22
+ } else {
23
+ this.handlers.onVisible();
24
+ }
25
+ }
26
+ }) : undefined;
27
+ const unbindPageHide = bind(window, {
28
+ type: 'pagehide',
29
+ listener: () => this.handlers.onPageHide()
30
+ });
31
+ const unbindPageShow = bind(window, {
32
+ type: 'pageshow',
33
+ listener: () => this.handlers.onPageShow()
34
+ });
35
+ this.unbind = () => {
36
+ unbindVisibilityChange === null || unbindVisibilityChange === void 0 ? void 0 : unbindVisibilityChange();
37
+ unbindPageHide();
38
+ unbindPageShow();
39
+ };
40
+ }
41
+ stop() {
42
+ var _this$unbind;
43
+ (_this$unbind = this.unbind) === null || _this$unbind === void 0 ? void 0 : _this$unbind.call(this);
44
+ this.unbind = undefined;
45
+ }
46
+ }
@@ -0,0 +1,48 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ /**
3
+ * Snapshots are taken 10 s, 30 s and 60 s after the session starts, then every 60 s. The
4
+ * first three come close together so that short sessions, which are the common case, are
5
+ * still reported.
6
+ */
7
+ const INITIAL_OFFSETS_MS = [10_000, 30_000, 60_000];
8
+ const INTERVAL_MS = 60_000;
9
+
10
+ /**
11
+ * Fires `onTick` on the snapshot cadence. Timings are offsets from `start()`, so a
12
+ * restart (a new session in the same editor) restarts the cadence too.
13
+ */
14
+ export class SnapshotScheduler {
15
+ constructor(onTick) {
16
+ _defineProperty(this, "tickIndex", 0);
17
+ this.onTick = onTick;
18
+ }
19
+ start() {
20
+ this.tickIndex = 0;
21
+ this.scheduleNext();
22
+ }
23
+ restart() {
24
+ this.stop();
25
+ this.start();
26
+ }
27
+ stop() {
28
+ if (this.timeoutId !== undefined) {
29
+ window.clearTimeout(this.timeoutId);
30
+ this.timeoutId = undefined;
31
+ }
32
+ }
33
+ scheduleNext() {
34
+ this.timeoutId = window.setTimeout(() => {
35
+ this.tickIndex += 1;
36
+ this.onTick();
37
+ this.scheduleNext();
38
+ }, this.delayForNextTick());
39
+ }
40
+ delayForNextTick() {
41
+ const offset = INITIAL_OFFSETS_MS[this.tickIndex];
42
+ if (offset === undefined) {
43
+ return INTERVAL_MS;
44
+ }
45
+ const previousOffset = this.tickIndex === 0 ? 0 : INITIAL_OFFSETS_MS[this.tickIndex - 1];
46
+ return offset - previousOffset;
47
+ }
48
+ }
@@ -1,10 +1,91 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { logException } from '@atlaskit/editor-common/monitoring';
3
+ import { fireInteractivityEvent } from './analytics/fire-interactivity-event';
4
+ import { InteractivityCollector } from './collector/interactivity-collector';
1
5
  /**
2
- * Reports interaction latency distributions for full page editor sessions as the
3
- * `editor interactivity` operational event.
6
+ * Reports session-to-date interaction latency distributions for full page editor sessions
7
+ * as the `editor interactivity` operational event.
4
8
  *
5
- * A stub for now: the plugin only registers itself. The collector and the event land in
6
- * EDITOR-8553.
9
+ * The session runs for as long as the plugin's hook stays mounted, which is one editor
10
+ * mount: `MountPluginHooks` in editor-core keys hook fibers by plugin name, so a preset
11
+ * reconfigure — which destroys and recreates ProseMirror plugin views — leaves this hook,
12
+ * and the session, in place.
7
13
  */
8
- export const interactivityPlugin = () => ({
9
- name: 'interactivity'
14
+ export const interactivityPlugin = ({
15
+ api
16
+ }) => ({
17
+ name: 'interactivity',
18
+ usePluginHook({
19
+ editorView,
20
+ wrapperElement
21
+ }) {
22
+ const collectorRef = useRef(undefined);
23
+ useEffect(() => {
24
+ try {
25
+ var _api$contextIdentifie4, _api$editorViewMode2;
26
+ const collector = new InteractivityCollector({
27
+ emit: snapshot => {
28
+ var _api$analytics;
29
+ return fireInteractivityEvent(api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions, snapshot);
30
+ },
31
+ getObjectId: () => {
32
+ var _api$contextIdentifie, _api$contextIdentifie2, _api$contextIdentifie3;
33
+ return api === null || api === void 0 ? void 0 : (_api$contextIdentifie = api.contextIdentifier) === null || _api$contextIdentifie === void 0 ? void 0 : (_api$contextIdentifie2 = _api$contextIdentifie.sharedState.currentState()) === null || _api$contextIdentifie2 === void 0 ? void 0 : (_api$contextIdentifie3 = _api$contextIdentifie2.contextIdentifierProvider) === null || _api$contextIdentifie3 === void 0 ? void 0 : _api$contextIdentifie3.objectId;
34
+ },
35
+ getSessionMode: () => {
36
+ var _api$editorViewMode, _api$editorViewMode$s;
37
+ const mode = api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : (_api$editorViewMode$s = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode$s === void 0 ? void 0 : _api$editorViewMode$s.mode;
38
+ if (mode === undefined) {
39
+ return undefined;
40
+ }
41
+ return mode === 'view' ? 'reading' : 'editing';
42
+ },
43
+ // A destroyed view still answers, with the document it was destroyed with, so
44
+ // both report nothing rather than a size the editor no longer has.
45
+ getNodeSize: () => editorView.isDestroyed ? undefined : editorView.state.doc.nodeSize,
46
+ // `getElementsByTagName` counts descendants in the browser engine, so this
47
+ // cannot overflow the stack on very large documents.
48
+ getEditorDomSize: () => editorView.isDestroyed ? undefined : editorView.dom.getElementsByTagName('*').length
49
+ });
50
+
51
+ // Effects run in declaration order, so the one below always finds it.
52
+ collectorRef.current = collector;
53
+ if (!collector.start()) {
54
+ return;
55
+ }
56
+
57
+ // Confluence live pages navigate and switch between reading and editing without
58
+ // remounting the editor. A session covers one document in one mode, so either
59
+ // change ends it and starts the next.
60
+ const unsubscribeFromObjectId = api === null || api === void 0 ? void 0 : (_api$contextIdentifie4 = api.contextIdentifier) === null || _api$contextIdentifie4 === void 0 ? void 0 : _api$contextIdentifie4.sharedState.onChange(() => collector.onObjectIdChanged());
61
+ const unsubscribeFromViewMode = api === null || api === void 0 ? void 0 : (_api$editorViewMode2 = api.editorViewMode) === null || _api$editorViewMode2 === void 0 ? void 0 : _api$editorViewMode2.sharedState.onChange(() => collector.onViewModeChanged());
62
+ return () => {
63
+ unsubscribeFromObjectId === null || unsubscribeFromObjectId === void 0 ? void 0 : unsubscribeFromObjectId();
64
+ unsubscribeFromViewMode === null || unsubscribeFromViewMode === void 0 ? void 0 : unsubscribeFromViewMode();
65
+ collector.stop();
66
+ };
67
+ } catch (error) {
68
+ // Instrumentation must not fail the editor around it, but a failure that hits every
69
+ // browser of one engine would be invisible without this.
70
+ void logException(error, {
71
+ location: 'editor-plugin-interactivity/start'
72
+ });
73
+ return;
74
+ }
75
+ // `api` is deliberately not a dependency: a preset reconfigure hands out a new
76
+ // proxy object, but the one captured here keeps resolving plugins from the same
77
+ // live registry, and restarting the session on a reconfigure would split one
78
+ // editor session in two.
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]);
90
+ }
10
91
  });
@@ -0,0 +1,33 @@
1
+ import { ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics/types/enums';
2
+ /** The reasons that mean the page may not be around long enough to deliver a queued event. */
3
+ var REASONS_THE_PAGE_MAY_NOT_OUTLIVE = ['hidden', 'pagehide', 'unmount'];
4
+
5
+ /**
6
+ * The `editor interactivity` operational event.
7
+ *
8
+ * `editorSessionId` arrives without being set here: `@atlaskit/analytics-listeners` merges
9
+ * the editor analytics context into the attributes of every event on the editor channel.
10
+ * `objectId` comes both from that context and from the snapshot, which carries the value
11
+ * the session was collected against.
12
+ */
13
+
14
+ /**
15
+ * Sends a snapshot as the `editor interactivity` event.
16
+ *
17
+ * Does nothing without the analytics plugin: an editor without it has nowhere to send events,
18
+ * and the collector keeps measuring either way.
19
+ */
20
+ export function fireInteractivityEvent(analytics, snapshot) {
21
+ analytics === null || analytics === void 0 || analytics.fireAnalyticsEvent({
22
+ action: 'interactivity',
23
+ actionSubject: ACTION_SUBJECT.EDITOR,
24
+ eventType: EVENT_TYPE.OPERATIONAL,
25
+ attributes: snapshot
26
+ }, undefined,
27
+ // Events are queued into an idle callback by default, which a page being unloaded never
28
+ // runs and a backgrounded tab throttles. The snapshots taken because the page is going
29
+ // away go immediately; the rest can wait their turn.
30
+ {
31
+ immediate: REASONS_THE_PAGE_MAY_NOT_OUTLIVE.includes(snapshot.reason)
32
+ });
33
+ }
File without changes
@@ -0,0 +1,130 @@
1
+ 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; } } }; }
2
+ 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; } }
3
+ 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; }
4
+ /**
5
+ * Bucket boundaries for the interaction latency buckets, version 1.
6
+ *
7
+ * Two ranges:
8
+ * - 16 ms to 200 ms — a boundary every 8 ms: 16, 24, 32, … 200. Event Timing rounds
9
+ * durations to 8 ms, so nothing finer is measurable.
10
+ * - above 200 ms — each boundary ~15% above the previous one, five of them per doubling:
11
+ * 222, 256, 294, 337, 388, 445, 512, … A 40 ms difference matters at 100 ms and is
12
+ * noise at 4 seconds, so buckets grow with the latency instead of staying 8 ms wide.
13
+ *
14
+ * 500 ms — the Google INP "poor" threshold — falls inside the 445…512 bucket, so that one
15
+ * bucket is split at 500 to count the threshold instead of interpolating it. 200 ms, the
16
+ * "good" threshold, is already a boundary.
17
+ *
18
+ * Bump SCHEMA_VERSION whenever any boundary moves; queries group by it.
19
+ */
20
+ export var SCHEMA_VERSION = 1;
21
+
22
+ /**
23
+ * Event Timing reporting threshold. Faster interactions are never delivered to the
24
+ * observer, so they reach no bucket at all — `performance.interactionCount` is what counts
25
+ * them, as `totalCount - observedCount`. This bucket holds the interactions reported at
26
+ * exactly the threshold.
27
+ */
28
+ export var REPORTING_THRESHOLD_MS = 16;
29
+
30
+ /**
31
+ * Exceptions to the grid: latencies that must be a boundary of their own so that they are
32
+ * counted exactly rather than read off a bucket that spans them. Each one splits the bucket it
33
+ * falls inside. 500 ms is the Google INP "poor" threshold; the "good" one, 200 ms, needs no
34
+ * exception because the evenly spaced range already ends there.
35
+ *
36
+ * Every entry has to sit above that range, and adding one changes the reported keys, so bump
37
+ * SCHEMA_VERSION with it.
38
+ */
39
+ var EXACT_THRESHOLDS_MS = [500];
40
+ var EVENLY_SPACED_MAX_MS = 200;
41
+ /**
42
+ * Keep this a multiple of 8 (8, 16, 24, …). Event Timing reports durations in 8 ms steps, so a
43
+ * step that is not a multiple of 8 leaves buckets no interaction can ever land in.
44
+ */
45
+ var EVENLY_SPACED_STEP_MS = 8;
46
+ var BOUNDARIES_PER_DOUBLING = 5;
47
+
48
+ /**
49
+ * The first boundary at or above `latencyMs`, for the range above 200 ms.
50
+ *
51
+ * Five boundaries per doubling is the same as saying the nth boundary sits at `2^(n/5)` ms —
52
+ * boundary 40 at 256 ms, 45 at 512 ms, 50 at 1024 ms. So this turns the latency into a
53
+ * boundary number, rounds that up, and turns it back into milliseconds.
54
+ *
55
+ * Boundaries are floored to whole milliseconds, which keeps each one at or below the exact
56
+ * value it stands for. That is what makes `bucketKeyForMs(boundary) === boundary` hold.
57
+ */
58
+ function firstBoundaryAtOrAbove(latencyMs) {
59
+ // `Math.log2(latencyMs) * 5` is the boundary number: log2 answers how many doublings of
60
+ // 1 ms reach this latency, and five boundaries cover each doubling.
61
+ //
62
+ // A latency sitting on a boundary makes that a whole number, which `Math.ceil` has to
63
+ // keep. ECMA-262 only requires `Math.log2` to be approximate, so 9.0000000000000002 for
64
+ // `log2(512)` would round up to boundary 46 and report 512 ms as 588 ms. EPSILON is
65
+ // larger than such imprecision and far smaller than the gap between two boundaries.
66
+ var EPSILON = 1e-9;
67
+ var boundaryNumber = Math.ceil(BOUNDARIES_PER_DOUBLING * Math.log2(latencyMs) - EPSILON);
68
+
69
+ // One `Math.pow` over the whole exponent, so the result is rounded once. Multiplying the
70
+ // ratio between neighbouring boundaries (2^(1/5) ≈ 1.1487) by itself instead accumulates
71
+ // the rounding of every step, reaching 512.0000000000018 by boundary 45.
72
+ return Math.floor(Math.pow(2, boundaryNumber / BOUNDARIES_PER_DOUBLING));
73
+ }
74
+
75
+ /** The bucket each exact threshold splits — 500 ms splits the one ending at 512 ms. */
76
+ var EXACT_THRESHOLD_BUCKETS = EXACT_THRESHOLDS_MS.map(function (thresholdMs) {
77
+ return {
78
+ thresholdMs: thresholdMs,
79
+ bucketMs: firstBoundaryAtOrAbove(thresholdMs)
80
+ };
81
+ });
82
+
83
+ /**
84
+ * The bucket a latency belongs to, identified by the bucket's upper boundary in whole
85
+ * milliseconds — which is also its key in the reported buckets.
86
+ *
87
+ * Every latency gets a bucket, however slow: the boundaries continue upwards, so there is no
88
+ * overflow bucket. Expects the whole-millisecond durations Event Timing reports; boundaries
89
+ * are floored, so a fractional latency can land in a bucket whose key reads up to a
90
+ * millisecond below it. Non-finite latencies never reach here — `InteractionTracker` drops
91
+ * them as it reads the entry.
92
+ */
93
+ export function bucketKeyForMs(latencyMs) {
94
+ if (latencyMs <= REPORTING_THRESHOLD_MS) {
95
+ return REPORTING_THRESHOLD_MS;
96
+ }
97
+ if (latencyMs <= EVENLY_SPACED_MAX_MS) {
98
+ // How many 8 ms steps above the threshold the latency is, rounded up: 17 ms is 0.125
99
+ // steps up and lands on the boundary one step up, 24 ms. Rounding up is what keeps the
100
+ // set of keys fixed when a latency is not a multiple of 8 ms.
101
+ //
102
+ // No EPSILON here, unlike the branch below: subtracting whole numbers gives a whole
103
+ // number, and dividing by a power of two shifts a binary float's exponent without
104
+ // touching its digits, so a latency on a boundary cannot come out just above a whole
105
+ // number of steps.
106
+ var steps = Math.ceil((latencyMs - REPORTING_THRESHOLD_MS) / EVENLY_SPACED_STEP_MS);
107
+ return REPORTING_THRESHOLD_MS + steps * EVENLY_SPACED_STEP_MS;
108
+ }
109
+ var boundary = firstBoundaryAtOrAbove(latencyMs);
110
+
111
+ // A latency in the lower part of a split bucket is reported as the threshold itself, so the
112
+ // threshold is counted exactly.
113
+ var _iterator = _createForOfIteratorHelper(EXACT_THRESHOLD_BUCKETS),
114
+ _step;
115
+ try {
116
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
117
+ var _step$value = _step.value,
118
+ thresholdMs = _step$value.thresholdMs,
119
+ bucketMs = _step$value.bucketMs;
120
+ if (boundary === bucketMs && latencyMs <= thresholdMs) {
121
+ return thresholdMs;
122
+ }
123
+ }
124
+ } catch (err) {
125
+ _iterator.e(err);
126
+ } finally {
127
+ _iterator.f();
128
+ }
129
+ return boundary;
130
+ }
@@ -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
+ }
@@ -0,0 +1,179 @@
1
+ import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
2
+ import _createClass from "@babel/runtime/helpers/createClass";
3
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
+ 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
+ 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
+ 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 { bucketKeyForMs } from './bucket-boundaries';
8
+
9
+ /**
10
+ * Latencies are counted per 8 ms, the resolution Event Timing reports durations at. Every
11
+ * latency is rounded up to this step on the way in, which caps the number of distinct values
12
+ * a group can hold whatever the latency was derived from.
13
+ */
14
+ var RESOLUTION_MS = 8;
15
+
16
+ /** Which percentiles are reported, as quantiles. */
17
+ var REPORTED_QUANTILES = [0.9, 0.98];
18
+
19
+ /**
20
+ * The latencies of one set of interactions — every interaction on the page, or only the ones
21
+ * inside the editor — reported as one object in the event.
22
+ *
23
+ * The whole state is a count per distinct latency, so an interaction only costs a counter
24
+ * whatever its latency was, and everything the event carries — the reported buckets, the
25
+ * count, the sum, the maximum and the percentiles — is derived from that map when a snapshot
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.
32
+ */
33
+ export var InteractionGroup = /*#__PURE__*/function () {
34
+ function InteractionGroup() {
35
+ _classCallCheck(this, InteractionGroup);
36
+ _defineProperty(this, "countByLatency", new Map());
37
+ _defineProperty(this, "totalCount", 0);
38
+ }
39
+ return _createClass(InteractionGroup, [{
40
+ key: "add",
41
+ value: function add(latencyMs) {
42
+ this.increment(latencyMs);
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
+ }
54
+ }, {
55
+ key: "remeasure",
56
+ value: function remeasure(previousLatencyMs, latencyMs) {
57
+ // Moved rather than counted again: the count belongs to the same interaction.
58
+ this.decrement(previousLatencyMs);
59
+ this.increment(latencyMs);
60
+ }
61
+
62
+ /**
63
+ * @param totalCount every interaction of the group, including those below the Event Timing
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.
66
+ */
67
+ }, {
68
+ key: "snapshot",
69
+ value: function snapshot() {
70
+ var _latencies;
71
+ var totalCount = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.totalCount;
72
+ // Ascending, so the reported buckets come out in order and the last latency is the
73
+ // maximum. Sorted once for everything below.
74
+ var latencies = Array.from(this.countByLatency.keys()).sort(function (a, b) {
75
+ return a - b;
76
+ });
77
+ var observedCount = this.observedCount();
78
+ var percentileRanks = REPORTED_QUANTILES.map(function (quantile) {
79
+ return {
80
+ key: String(Math.round(quantile * 100)),
81
+ rank: Math.max(1, Math.ceil(quantile * observedCount))
82
+ };
83
+ });
84
+ var buckets = {};
85
+ var percentilesMs = {};
86
+ var sumMs = 0;
87
+ var counted = 0;
88
+ var _iterator = _createForOfIteratorHelper(latencies),
89
+ _step;
90
+ try {
91
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
92
+ var _this$countByLatency$, _buckets$bucket;
93
+ var latencyMs = _step.value;
94
+ var count = (_this$countByLatency$ = this.countByLatency.get(latencyMs)) !== null && _this$countByLatency$ !== void 0 ? _this$countByLatency$ : 0;
95
+ sumMs += latencyMs * count;
96
+ var bucket = String(bucketKeyForMs(latencyMs));
97
+ buckets[bucket] = ((_buckets$bucket = buckets[bucket]) !== null && _buckets$bucket !== void 0 ? _buckets$bucket : 0) + count;
98
+
99
+ // A percentile is the latency the group's interactions reach counting up from the
100
+ // fastest, so it is answered as soon as this many of them have been passed.
101
+ counted += count;
102
+ var _iterator2 = _createForOfIteratorHelper(percentileRanks),
103
+ _step2;
104
+ try {
105
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
106
+ var _step2$value = _step2.value,
107
+ key = _step2$value.key,
108
+ rank = _step2$value.rank;
109
+ if (percentilesMs[key] === undefined && counted >= rank) {
110
+ percentilesMs[key] = latencyMs;
111
+ }
112
+ }
113
+ } catch (err) {
114
+ _iterator2.e(err);
115
+ } finally {
116
+ _iterator2.f();
117
+ }
118
+ }
119
+ } catch (err) {
120
+ _iterator.e(err);
121
+ } finally {
122
+ _iterator.f();
123
+ }
124
+ return {
125
+ // `performance.interactionCount` can lag the entries the observer has delivered.
126
+ totalCount: Math.max(totalCount, observedCount),
127
+ observedCount: observedCount,
128
+ sumMs: sumMs,
129
+ maxMs: (_latencies = latencies[latencies.length - 1]) !== null && _latencies !== void 0 ? _latencies : 0,
130
+ buckets: buckets,
131
+ percentilesMs: percentilesMs
132
+ };
133
+ }
134
+ }, {
135
+ key: "observedCount",
136
+ value: function observedCount() {
137
+ var total = 0;
138
+ var _iterator3 = _createForOfIteratorHelper(this.countByLatency.values()),
139
+ _step3;
140
+ try {
141
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
142
+ var count = _step3.value;
143
+ total += count;
144
+ }
145
+ } catch (err) {
146
+ _iterator3.e(err);
147
+ } finally {
148
+ _iterator3.f();
149
+ }
150
+ return total;
151
+ }
152
+
153
+ /** Rounding lives here so that every count goes through the same step, in or out. */
154
+ }, {
155
+ key: "roundLatencyUp",
156
+ value: function roundLatencyUp(latencyMs) {
157
+ return Math.ceil(latencyMs / RESOLUTION_MS) * RESOLUTION_MS;
158
+ }
159
+ }, {
160
+ key: "increment",
161
+ value: function increment(latencyMs) {
162
+ var _this$countByLatency$2;
163
+ var step = this.roundLatencyUp(latencyMs);
164
+ this.countByLatency.set(step, ((_this$countByLatency$2 = this.countByLatency.get(step)) !== null && _this$countByLatency$2 !== void 0 ? _this$countByLatency$2 : 0) + 1);
165
+ }
166
+ }, {
167
+ key: "decrement",
168
+ value: function decrement(latencyMs) {
169
+ var _this$countByLatency$3;
170
+ var step = this.roundLatencyUp(latencyMs);
171
+ var next = ((_this$countByLatency$3 = this.countByLatency.get(step)) !== null && _this$countByLatency$3 !== void 0 ? _this$countByLatency$3 : 0) - 1;
172
+ if (next > 0) {
173
+ this.countByLatency.set(step, next);
174
+ } else {
175
+ this.countByLatency.delete(step);
176
+ }
177
+ }
178
+ }]);
179
+ }();