@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.
- package/CHANGELOG.md +26 -0
- package/README.md +65 -6
- package/afm-cc/tsconfig.json +12 -0
- package/afm-products/tsconfig.json +12 -0
- package/dist/cjs/analytics/fire-interactivity-event.js +39 -0
- package/dist/cjs/analytics/interactivity-snapshot.js +1 -0
- package/dist/cjs/collector/bucket-boundaries.js +137 -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 +185 -0
- package/dist/cjs/collector/interaction-observer.js +114 -0
- package/dist/cjs/collector/interaction-tracker.js +185 -0
- package/dist/cjs/collector/interactivity-collector.js +341 -0
- package/dist/cjs/collector/interactivity-session.js +55 -0
- package/dist/cjs/collector/lifecycle-observer.js +66 -0
- package/dist/cjs/collector/snapshot-scheduler.js +70 -0
- package/dist/cjs/interactivityPlugin.js +93 -6
- package/dist/es2019/analytics/fire-interactivity-event.js +33 -0
- package/dist/es2019/analytics/interactivity-snapshot.js +0 -0
- package/dist/es2019/collector/bucket-boundaries.js +117 -0
- 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 +125 -0
- package/dist/es2019/collector/interaction-observer.js +90 -0
- package/dist/es2019/collector/interaction-tracker.js +156 -0
- package/dist/es2019/collector/interactivity-collector.js +274 -0
- package/dist/es2019/collector/interactivity-session.js +45 -0
- package/dist/es2019/collector/lifecycle-observer.js +46 -0
- package/dist/es2019/collector/snapshot-scheduler.js +48 -0
- package/dist/es2019/interactivityPlugin.js +87 -6
- package/dist/esm/analytics/fire-interactivity-event.js +33 -0
- package/dist/esm/analytics/interactivity-snapshot.js +0 -0
- package/dist/esm/collector/bucket-boundaries.js +130 -0
- 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 +179 -0
- package/dist/esm/collector/interaction-observer.js +108 -0
- package/dist/esm/collector/interaction-tracker.js +179 -0
- package/dist/esm/collector/interactivity-collector.js +334 -0
- package/dist/esm/collector/interactivity-session.js +48 -0
- package/dist/esm/collector/lifecycle-observer.js +59 -0
- package/dist/esm/collector/snapshot-scheduler.js +63 -0
- package/dist/esm/interactivityPlugin.js +93 -6
- package/dist/types/analytics/fire-interactivity-event.d.ts +9 -0
- package/dist/types/analytics/interactivity-snapshot.d.ts +66 -0
- package/dist/types/collector/bucket-boundaries.d.ts +35 -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 +37 -0
- package/dist/types/collector/interaction-observer.d.ts +39 -0
- package/dist/types/collector/interaction-tracker.d.ts +65 -0
- package/dist/types/collector/interactivity-collector.d.ts +90 -0
- package/dist/types/collector/interactivity-session.d.ts +42 -0
- package/dist/types/collector/lifecycle-observer.d.ts +25 -0
- package/dist/types/collector/snapshot-scheduler.d.ts +15 -0
- package/dist/types/interactivityPlugin.d.ts +6 -4
- package/dist/types/interactivityPluginType.d.ts +11 -2
- package/docs/0-intro.tsx +26 -7
- package/package.json +8 -3
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
|
|
2
|
+
import _createClass from "@babel/runtime/helpers/createClass";
|
|
3
|
+
import { bind } from 'bind-event-listener';
|
|
4
|
+
import { getDocument } from '@atlaskit/browser-apis';
|
|
5
|
+
/**
|
|
6
|
+
* Reports the page lifecycle signals that force an extra snapshot: the tab being hidden or
|
|
7
|
+
* shown again, and the page being unloaded or restored from the back/forward cache.
|
|
8
|
+
*
|
|
9
|
+
* None of them is guaranteed to arrive — a mobile browser being killed raises nothing at all.
|
|
10
|
+
* Snapshots carry session-to-date values, so losing the last one costs only the tail of that
|
|
11
|
+
* session.
|
|
12
|
+
*/
|
|
13
|
+
export var LifecycleObserver = /*#__PURE__*/function () {
|
|
14
|
+
function LifecycleObserver(handlers) {
|
|
15
|
+
_classCallCheck(this, LifecycleObserver);
|
|
16
|
+
this.handlers = handlers;
|
|
17
|
+
}
|
|
18
|
+
return _createClass(LifecycleObserver, [{
|
|
19
|
+
key: "start",
|
|
20
|
+
value: function start() {
|
|
21
|
+
var _this = this;
|
|
22
|
+
var doc = getDocument();
|
|
23
|
+
var unbindVisibilityChange = doc ? bind(doc, {
|
|
24
|
+
type: 'visibilitychange',
|
|
25
|
+
listener: function listener() {
|
|
26
|
+
if (doc.visibilityState === 'hidden') {
|
|
27
|
+
_this.handlers.onHidden();
|
|
28
|
+
} else {
|
|
29
|
+
_this.handlers.onVisible();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}) : undefined;
|
|
33
|
+
var unbindPageHide = bind(window, {
|
|
34
|
+
type: 'pagehide',
|
|
35
|
+
listener: function listener() {
|
|
36
|
+
return _this.handlers.onPageHide();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
var unbindPageShow = bind(window, {
|
|
40
|
+
type: 'pageshow',
|
|
41
|
+
listener: function listener() {
|
|
42
|
+
return _this.handlers.onPageShow();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
this.unbind = function () {
|
|
46
|
+
unbindVisibilityChange === null || unbindVisibilityChange === void 0 || unbindVisibilityChange();
|
|
47
|
+
unbindPageHide();
|
|
48
|
+
unbindPageShow();
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}, {
|
|
52
|
+
key: "stop",
|
|
53
|
+
value: function stop() {
|
|
54
|
+
var _this$unbind;
|
|
55
|
+
(_this$unbind = this.unbind) === null || _this$unbind === void 0 || _this$unbind.call(this);
|
|
56
|
+
this.unbind = undefined;
|
|
57
|
+
}
|
|
58
|
+
}]);
|
|
59
|
+
}();
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Snapshots are taken 10 s, 30 s and 60 s after the session starts, then every 60 s. The
|
|
6
|
+
* first three come close together so that short sessions, which are the common case, are
|
|
7
|
+
* still reported.
|
|
8
|
+
*/
|
|
9
|
+
var INITIAL_OFFSETS_MS = [10000, 30000, 60000];
|
|
10
|
+
var INTERVAL_MS = 60000;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fires `onTick` on the snapshot cadence. Timings are offsets from `start()`, so a
|
|
14
|
+
* restart (a new session in the same editor) restarts the cadence too.
|
|
15
|
+
*/
|
|
16
|
+
export var SnapshotScheduler = /*#__PURE__*/function () {
|
|
17
|
+
function SnapshotScheduler(onTick) {
|
|
18
|
+
_classCallCheck(this, SnapshotScheduler);
|
|
19
|
+
_defineProperty(this, "tickIndex", 0);
|
|
20
|
+
this.onTick = onTick;
|
|
21
|
+
}
|
|
22
|
+
return _createClass(SnapshotScheduler, [{
|
|
23
|
+
key: "start",
|
|
24
|
+
value: function start() {
|
|
25
|
+
this.tickIndex = 0;
|
|
26
|
+
this.scheduleNext();
|
|
27
|
+
}
|
|
28
|
+
}, {
|
|
29
|
+
key: "restart",
|
|
30
|
+
value: function restart() {
|
|
31
|
+
this.stop();
|
|
32
|
+
this.start();
|
|
33
|
+
}
|
|
34
|
+
}, {
|
|
35
|
+
key: "stop",
|
|
36
|
+
value: function stop() {
|
|
37
|
+
if (this.timeoutId !== undefined) {
|
|
38
|
+
window.clearTimeout(this.timeoutId);
|
|
39
|
+
this.timeoutId = undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, {
|
|
43
|
+
key: "scheduleNext",
|
|
44
|
+
value: function scheduleNext() {
|
|
45
|
+
var _this = this;
|
|
46
|
+
this.timeoutId = window.setTimeout(function () {
|
|
47
|
+
_this.tickIndex += 1;
|
|
48
|
+
_this.onTick();
|
|
49
|
+
_this.scheduleNext();
|
|
50
|
+
}, this.delayForNextTick());
|
|
51
|
+
}
|
|
52
|
+
}, {
|
|
53
|
+
key: "delayForNextTick",
|
|
54
|
+
value: function delayForNextTick() {
|
|
55
|
+
var offset = INITIAL_OFFSETS_MS[this.tickIndex];
|
|
56
|
+
if (offset === undefined) {
|
|
57
|
+
return INTERVAL_MS;
|
|
58
|
+
}
|
|
59
|
+
var previousOffset = this.tickIndex === 0 ? 0 : INITIAL_OFFSETS_MS[this.tickIndex - 1];
|
|
60
|
+
return offset - previousOffset;
|
|
61
|
+
}
|
|
62
|
+
}]);
|
|
63
|
+
}();
|
|
@@ -1,12 +1,99 @@
|
|
|
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
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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 var interactivityPlugin = function interactivityPlugin() {
|
|
14
|
+
export var interactivityPlugin = function interactivityPlugin(_ref) {
|
|
15
|
+
var api = _ref.api;
|
|
9
16
|
return {
|
|
10
|
-
name: 'interactivity'
|
|
17
|
+
name: 'interactivity',
|
|
18
|
+
usePluginHook: function usePluginHook(_ref2) {
|
|
19
|
+
var editorView = _ref2.editorView,
|
|
20
|
+
wrapperElement = _ref2.wrapperElement;
|
|
21
|
+
var collectorRef = useRef(undefined);
|
|
22
|
+
useEffect(function () {
|
|
23
|
+
try {
|
|
24
|
+
var _api$contextIdentifie2, _api$editorViewMode2;
|
|
25
|
+
var collector = new InteractivityCollector({
|
|
26
|
+
emit: function emit(snapshot) {
|
|
27
|
+
var _api$analytics;
|
|
28
|
+
return fireInteractivityEvent(api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions, snapshot);
|
|
29
|
+
},
|
|
30
|
+
getObjectId: function getObjectId() {
|
|
31
|
+
var _api$contextIdentifie;
|
|
32
|
+
return api === null || api === void 0 || (_api$contextIdentifie = api.contextIdentifier) === null || _api$contextIdentifie === void 0 || (_api$contextIdentifie = _api$contextIdentifie.sharedState.currentState()) === null || _api$contextIdentifie === void 0 || (_api$contextIdentifie = _api$contextIdentifie.contextIdentifierProvider) === null || _api$contextIdentifie === void 0 ? void 0 : _api$contextIdentifie.objectId;
|
|
33
|
+
},
|
|
34
|
+
getSessionMode: function getSessionMode() {
|
|
35
|
+
var _api$editorViewMode;
|
|
36
|
+
var mode = api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode;
|
|
37
|
+
if (mode === undefined) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return mode === 'view' ? 'reading' : 'editing';
|
|
41
|
+
},
|
|
42
|
+
// A destroyed view still answers, with the document it was destroyed with, so
|
|
43
|
+
// both report nothing rather than a size the editor no longer has.
|
|
44
|
+
getNodeSize: function getNodeSize() {
|
|
45
|
+
return editorView.isDestroyed ? undefined : editorView.state.doc.nodeSize;
|
|
46
|
+
},
|
|
47
|
+
// `getElementsByTagName` counts descendants in the browser engine, so this
|
|
48
|
+
// cannot overflow the stack on very large documents.
|
|
49
|
+
getEditorDomSize: function getEditorDomSize() {
|
|
50
|
+
return editorView.isDestroyed ? undefined : editorView.dom.getElementsByTagName('*').length;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Effects run in declaration order, so the one below always finds it.
|
|
55
|
+
collectorRef.current = collector;
|
|
56
|
+
if (!collector.start()) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Confluence live pages navigate and switch between reading and editing without
|
|
61
|
+
// remounting the editor. A session covers one document in one mode, so either
|
|
62
|
+
// change ends it and starts the next.
|
|
63
|
+
var unsubscribeFromObjectId = api === null || api === void 0 || (_api$contextIdentifie2 = api.contextIdentifier) === null || _api$contextIdentifie2 === void 0 ? void 0 : _api$contextIdentifie2.sharedState.onChange(function () {
|
|
64
|
+
return collector.onObjectIdChanged();
|
|
65
|
+
});
|
|
66
|
+
var unsubscribeFromViewMode = api === null || api === void 0 || (_api$editorViewMode2 = api.editorViewMode) === null || _api$editorViewMode2 === void 0 ? void 0 : _api$editorViewMode2.sharedState.onChange(function () {
|
|
67
|
+
return collector.onViewModeChanged();
|
|
68
|
+
});
|
|
69
|
+
return function () {
|
|
70
|
+
unsubscribeFromObjectId === null || unsubscribeFromObjectId === void 0 || unsubscribeFromObjectId();
|
|
71
|
+
unsubscribeFromViewMode === null || unsubscribeFromViewMode === void 0 || unsubscribeFromViewMode();
|
|
72
|
+
collector.stop();
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Instrumentation must not fail the editor around it, but a failure that hits every
|
|
76
|
+
// browser of one engine would be invisible without this.
|
|
77
|
+
void logException(error, {
|
|
78
|
+
location: 'editor-plugin-interactivity/start'
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// `api` is deliberately not a dependency: a preset reconfigure hands out a new
|
|
83
|
+
// proxy object, but the one captured here keeps resolving plugins from the same
|
|
84
|
+
// live registry, and restarting the session on a reconfigure would split one
|
|
85
|
+
// editor session in two.
|
|
86
|
+
}, [editorView]);
|
|
87
|
+
useEffect(function () {
|
|
88
|
+
try {
|
|
89
|
+
var _collectorRef$current;
|
|
90
|
+
(_collectorRef$current = collectorRef.current) === null || _collectorRef$current === void 0 || _collectorRef$current.setEditorRoot(wrapperElement);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
void logException(error, {
|
|
93
|
+
location: 'editor-plugin-interactivity/observe'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}, [wrapperElement]);
|
|
97
|
+
}
|
|
11
98
|
};
|
|
12
99
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EditorAnalyticsAPI } from '@atlaskit/editor-common/analytics/api';
|
|
2
|
+
import type { InteractivitySnapshot } from './interactivity-snapshot';
|
|
3
|
+
/**
|
|
4
|
+
* Sends a snapshot as the `editor interactivity` event.
|
|
5
|
+
*
|
|
6
|
+
* Does nothing without the analytics plugin: an editor without it has nowhere to send events,
|
|
7
|
+
* and the collector keeps measuring either way.
|
|
8
|
+
*/
|
|
9
|
+
export declare function fireInteractivityEvent(analytics: EditorAnalyticsAPI | undefined, snapshot: InteractivitySnapshot): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of what the `editor interactivity` event carries: one snapshot, the groups inside
|
|
3
|
+
* it, and the values its fields can take.
|
|
4
|
+
*
|
|
5
|
+
* This is the contract with the analytics pipeline, which the collector fills in and the
|
|
6
|
+
* payload wraps, and the one place in the package where types are shared. A type serving a
|
|
7
|
+
* single module stays with that module.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* What produced a snapshot.
|
|
11
|
+
*
|
|
12
|
+
* `navigation` and `modeChange` end a session while the editor stays mounted: Confluence
|
|
13
|
+
* live pages keep one editor across page transitions and switch between reading and
|
|
14
|
+
* editing with a command. A session covers one document in one mode, so either change
|
|
15
|
+
* closes it and opens the next.
|
|
16
|
+
*/
|
|
17
|
+
export type SnapshotReason = 'timer' | 'hidden' | 'pagehide' | 'unmount' | 'navigation' | 'modeChange';
|
|
18
|
+
/**
|
|
19
|
+
* What the editor was being used for at the time of the snapshot: `editing` is the
|
|
20
|
+
* editable editor, `reading` is a live page being viewed with the editor still mounted.
|
|
21
|
+
*/
|
|
22
|
+
export type SessionMode = 'editing' | 'reading';
|
|
23
|
+
/**
|
|
24
|
+
* Session-to-date latency distribution for one group of interactions.
|
|
25
|
+
*
|
|
26
|
+
* `totalCount` counts every interaction, including those below the Event Timing reporting
|
|
27
|
+
* threshold, so `totalCount - observedCount` is the sub-threshold count. `buckets` is keyed by
|
|
28
|
+
* each bucket's upper boundary in milliseconds and is not cumulative; empty buckets are
|
|
29
|
+
* omitted, so a missing bucket means zero.
|
|
30
|
+
*/
|
|
31
|
+
export type InteractionGroupSnapshot = {
|
|
32
|
+
buckets: Record<string, number>;
|
|
33
|
+
maxMs: number;
|
|
34
|
+
observedCount: number;
|
|
35
|
+
/**
|
|
36
|
+
* Temporary. Percentiles of the same interactions, keyed by percentile and exact to the 8 ms
|
|
37
|
+
* Event Timing reports durations at, to confirm that a percentile read off `buckets` lands
|
|
38
|
+
* where the latencies actually are. Goes once that is established.
|
|
39
|
+
*/
|
|
40
|
+
percentilesMs: Record<string, number>;
|
|
41
|
+
sumMs: number;
|
|
42
|
+
totalCount: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* One session-to-date snapshot. Consumers take the highest `seq` per
|
|
46
|
+
* `interactivitySessionId` and then sum bucket counts across sessions.
|
|
47
|
+
*/
|
|
48
|
+
export type InteractivitySnapshot = {
|
|
49
|
+
activeMs: number;
|
|
50
|
+
editorDomSize?: number;
|
|
51
|
+
/** Interactions inside the editor that are neither typing nor pointing. */
|
|
52
|
+
editorOther: InteractionGroupSnapshot;
|
|
53
|
+
editorPointer: InteractionGroupSnapshot;
|
|
54
|
+
editorTyping: InteractionGroupSnapshot;
|
|
55
|
+
hiddenMs: number;
|
|
56
|
+
interactivitySessionId: string;
|
|
57
|
+
nodeSize?: number;
|
|
58
|
+
objectId?: string;
|
|
59
|
+
/** Every interaction on the page, the editor groups included. */
|
|
60
|
+
page: InteractionGroupSnapshot;
|
|
61
|
+
reason: SnapshotReason;
|
|
62
|
+
schema: number;
|
|
63
|
+
seq: number;
|
|
64
|
+
/** Fixed for the whole session: a mode change closes it and opens the next. */
|
|
65
|
+
sessionMode?: SessionMode;
|
|
66
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bucket boundaries for the interaction latency buckets, version 1.
|
|
3
|
+
*
|
|
4
|
+
* Two ranges:
|
|
5
|
+
* - 16 ms to 200 ms — a boundary every 8 ms: 16, 24, 32, … 200. Event Timing rounds
|
|
6
|
+
* durations to 8 ms, so nothing finer is measurable.
|
|
7
|
+
* - above 200 ms — each boundary ~15% above the previous one, five of them per doubling:
|
|
8
|
+
* 222, 256, 294, 337, 388, 445, 512, … A 40 ms difference matters at 100 ms and is
|
|
9
|
+
* noise at 4 seconds, so buckets grow with the latency instead of staying 8 ms wide.
|
|
10
|
+
*
|
|
11
|
+
* 500 ms — the Google INP "poor" threshold — falls inside the 445…512 bucket, so that one
|
|
12
|
+
* bucket is split at 500 to count the threshold instead of interpolating it. 200 ms, the
|
|
13
|
+
* "good" threshold, is already a boundary.
|
|
14
|
+
*
|
|
15
|
+
* Bump SCHEMA_VERSION whenever any boundary moves; queries group by it.
|
|
16
|
+
*/
|
|
17
|
+
export declare const SCHEMA_VERSION = 1;
|
|
18
|
+
/**
|
|
19
|
+
* Event Timing reporting threshold. Faster interactions are never delivered to the
|
|
20
|
+
* observer, so they reach no bucket at all — `performance.interactionCount` is what counts
|
|
21
|
+
* them, as `totalCount - observedCount`. This bucket holds the interactions reported at
|
|
22
|
+
* exactly the threshold.
|
|
23
|
+
*/
|
|
24
|
+
export declare const REPORTING_THRESHOLD_MS = 16;
|
|
25
|
+
/**
|
|
26
|
+
* The bucket a latency belongs to, identified by the bucket's upper boundary in whole
|
|
27
|
+
* milliseconds — which is also its key in the reported buckets.
|
|
28
|
+
*
|
|
29
|
+
* Every latency gets a bucket, however slow: the boundaries continue upwards, so there is no
|
|
30
|
+
* overflow bucket. Expects the whole-millisecond durations Event Timing reports; boundaries
|
|
31
|
+
* are floored, so a fractional latency can land in a bucket whose key reads up to a
|
|
32
|
+
* millisecond below it. Non-finite latencies never reach here — `InteractionTracker` drops
|
|
33
|
+
* them as it reads the entry.
|
|
34
|
+
*/
|
|
35
|
+
export declare function bucketKeyForMs(latencyMs: number): number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports the events the interactions with the editor are made of, as the browser dispatched
|
|
3
|
+
* them. What they mean is the tracker's business.
|
|
4
|
+
*
|
|
5
|
+
* Event Timing cannot answer which interactions were with the editor, because it observes the
|
|
6
|
+
* whole document, nor how many there were, because it reports none below 16 ms.
|
|
7
|
+
*/
|
|
8
|
+
export declare class EditorEventObserver {
|
|
9
|
+
private readonly onEvent;
|
|
10
|
+
private root;
|
|
11
|
+
private unbind;
|
|
12
|
+
constructor(onEvent: (event: Event) => void);
|
|
13
|
+
/**
|
|
14
|
+
* Called as the element the editor renders itself into changes: the editor only knows it after
|
|
15
|
+
* its first render, and can replace it.
|
|
16
|
+
*/
|
|
17
|
+
observe(root: Element | null | undefined): void;
|
|
18
|
+
stop(): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
export type EditorInteractionGroupName = 'editorOther' | 'editorPointer' | 'editorTyping';
|
|
8
|
+
export type InteractionEventKind = {
|
|
9
|
+
/**
|
|
10
|
+
* Whether an interaction is counted when this event arrives. One event of each group has it,
|
|
11
|
+
* because an interaction is several events — `keydown`, what the key produced, `keyup`, or
|
|
12
|
+
* `pointerdown`, `pointerup`, `click` — and counting each would count it three times.
|
|
13
|
+
*/
|
|
14
|
+
counts?: boolean;
|
|
15
|
+
group: EditorInteractionGroupName;
|
|
16
|
+
};
|
|
17
|
+
/** Derived, so the types listened for cannot drift from the table. */
|
|
18
|
+
export declare const INTERACTION_EVENT_TYPES: string[];
|
|
19
|
+
export declare function interactionEventKind(type: string): InteractionEventKind | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { InteractionGroupSnapshot } from '../analytics/interactivity-snapshot';
|
|
2
|
+
/**
|
|
3
|
+
* The latencies of one set of interactions — every interaction on the page, or only the ones
|
|
4
|
+
* inside the editor — reported as one object in the event.
|
|
5
|
+
*
|
|
6
|
+
* The whole state is a count per distinct latency, so an interaction only costs a counter
|
|
7
|
+
* whatever its latency was, and everything the event carries — the reported buckets, the
|
|
8
|
+
* count, the sum, the maximum and the percentiles — is derived from that map when a snapshot
|
|
9
|
+
* is taken. Nothing is computed while interactions arrive.
|
|
10
|
+
*
|
|
11
|
+
* The two counters count different populations: `add` takes the interactions Event Timing
|
|
12
|
+
* measured, `countTotal` takes all of them, including the ones below the 16 ms reporting threshold
|
|
13
|
+
* it never delivers. So `totalCount >= observedCount`, and the difference is how many were too
|
|
14
|
+
* fast to be measured.
|
|
15
|
+
*/
|
|
16
|
+
export declare class InteractionGroup {
|
|
17
|
+
private countByLatency;
|
|
18
|
+
private totalCount;
|
|
19
|
+
add(latencyMs: number): void;
|
|
20
|
+
/**
|
|
21
|
+
* Counts an interaction towards the group's total, measured or not. `page` has no use for it:
|
|
22
|
+
* `performance.interactionCount` counts the page's interactions.
|
|
23
|
+
*/
|
|
24
|
+
countTotal(): void;
|
|
25
|
+
remeasure(previousLatencyMs: number, latencyMs: number): void;
|
|
26
|
+
/**
|
|
27
|
+
* @param totalCount every interaction of the group, including those below the Event Timing
|
|
28
|
+
* reporting threshold. Defaults to what `countTotal` was told, which is where an editor
|
|
29
|
+
* group's total comes from; `page` passes `performance.interactionCount` instead.
|
|
30
|
+
*/
|
|
31
|
+
snapshot(totalCount?: number): InteractionGroupSnapshot;
|
|
32
|
+
private observedCount;
|
|
33
|
+
/** Rounding lives here so that every count goes through the same step, in or out. */
|
|
34
|
+
private roundLatencyUp;
|
|
35
|
+
private increment;
|
|
36
|
+
private decrement;
|
|
37
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { InteractionEntry } from './interaction-tracker';
|
|
2
|
+
/**
|
|
3
|
+
* Reports the interactions the browser observes to `onEntries`.
|
|
4
|
+
*
|
|
5
|
+
* `drain` and `stop` are safe to call before `start` and after each other, so a session
|
|
6
|
+
* that never started collecting needs no special handling.
|
|
7
|
+
*/
|
|
8
|
+
export declare class InteractionObserver {
|
|
9
|
+
/**
|
|
10
|
+
* Whether the browser reports both things a session needs: `interactionId`, which groups
|
|
11
|
+
* entries into interactions, and `performance.interactionCount`, which counts the ones
|
|
12
|
+
* below the reporting threshold. Both are Chromium-only, and without the count
|
|
13
|
+
* `totalCount` would be indistinguishable from `observedCount`.
|
|
14
|
+
*/
|
|
15
|
+
static isSupported(): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Total interactions on the page since page load, including those below the reporting
|
|
18
|
+
* threshold that no observer ever sees. A property of the page, not of an observer.
|
|
19
|
+
*/
|
|
20
|
+
static readPageInteractionCount(): number;
|
|
21
|
+
private readonly onEntries;
|
|
22
|
+
private observer;
|
|
23
|
+
constructor(onEntries: (entries: InteractionEntry[]) => void);
|
|
24
|
+
/**
|
|
25
|
+
* Starts reporting interactions from this point on. Does nothing when already started, so
|
|
26
|
+
* a second call cannot leave an observer running with nobody to disconnect it.
|
|
27
|
+
*
|
|
28
|
+
* `buffered` is `false`: the entries the browser collected earlier are interactions with
|
|
29
|
+
* the page while the editor was still loading, and they belong to no session of ours.
|
|
30
|
+
*/
|
|
31
|
+
start(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Synchronously reports the entries the browser has produced but not yet dispatched to
|
|
34
|
+
* the callback. A snapshot taken because the page is going away has to include them,
|
|
35
|
+
* because there is no later chance to.
|
|
36
|
+
*/
|
|
37
|
+
drain(): void;
|
|
38
|
+
stop(): void;
|
|
39
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { EditorInteractionGroupName } from './interaction-events';
|
|
2
|
+
/**
|
|
3
|
+
* The Event Timing fields this package reads. `interactionId` is missing from the DOM
|
|
4
|
+
* typings' `PerformanceEventTiming`, so it is declared here as optional, which also makes
|
|
5
|
+
* a `PerformanceEntry` from `getEntries()` assignable without a cast.
|
|
6
|
+
*/
|
|
7
|
+
export type InteractionEntry = PerformanceEntry & {
|
|
8
|
+
interactionId?: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* What an entry did to the interaction it belongs to: either it is the first entry of a new
|
|
12
|
+
* interaction, or it measured an interaction that was already counted as slower than it was
|
|
13
|
+
* known to be. Both carry the editor group of the interaction, if it is one of the editor's.
|
|
14
|
+
*/
|
|
15
|
+
export type InteractionUpdate = {
|
|
16
|
+
group: EditorInteractionGroupName | undefined;
|
|
17
|
+
latencyMs: number;
|
|
18
|
+
type: 'new';
|
|
19
|
+
} | {
|
|
20
|
+
fromMs: number;
|
|
21
|
+
group: EditorInteractionGroupName | undefined;
|
|
22
|
+
toMs: number;
|
|
23
|
+
type: 'remeasured';
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Makes interactions out of what the two observers report, for one session.
|
|
27
|
+
*
|
|
28
|
+
* Entries sharing a non-zero `interactionId` are one interaction whose latency is the
|
|
29
|
+
* maximum `duration` among them. Entries arrive incrementally, so an interaction's latency
|
|
30
|
+
* can grow after it was first reported — callers apply that to what they already counted
|
|
31
|
+
* rather than counting the interaction twice.
|
|
32
|
+
*
|
|
33
|
+
* A non-zero `interactionId` is the browser's own definition of an interaction, which is
|
|
34
|
+
* also what INP filters on: it is assigned to the pointer and keyboard events that make one
|
|
35
|
+
* up, and never to scrolling or pointer movement.
|
|
36
|
+
*
|
|
37
|
+
* The editor's events answer what an entry cannot: which interactions were with the editor, and
|
|
38
|
+
* how many there were, including the ones below the Event Timing reporting threshold.
|
|
39
|
+
*/
|
|
40
|
+
export declare class InteractionTracker {
|
|
41
|
+
private readonly startsAfterInteractionId;
|
|
42
|
+
private readonly interactions;
|
|
43
|
+
private readonly groupByEvent;
|
|
44
|
+
private highestInteractionId;
|
|
45
|
+
/**
|
|
46
|
+
* @param startsAfterInteractionId interactions up to and including this one belong to the
|
|
47
|
+
* previous tracker and are ignored. `interactionId` counts up over the life of the page, so
|
|
48
|
+
* a session opening mid-page passes the highest id the one before it saw; without that, an
|
|
49
|
+
* entry still arriving for an interaction from the previous session would look new here and
|
|
50
|
+
* be counted in both.
|
|
51
|
+
*/
|
|
52
|
+
constructor(startsAfterInteractionId?: number);
|
|
53
|
+
/** The highest `interactionId` this tracker has seen. */
|
|
54
|
+
get lastInteractionId(): number;
|
|
55
|
+
/**
|
|
56
|
+
* Merges an entry into the interaction it belongs to.
|
|
57
|
+
*
|
|
58
|
+
* @returns what that did to the interaction's latency, or nothing when the entry is not
|
|
59
|
+
* part of an interaction, belongs to a previous tracker, or does not change one.
|
|
60
|
+
*/
|
|
61
|
+
merge(entry: InteractionEntry): InteractionUpdate | undefined;
|
|
62
|
+
/** @returns the group of an interaction to count, when this is the event its group counts on. */
|
|
63
|
+
recordEditorEvent(event: Event): EditorInteractionGroupName | undefined;
|
|
64
|
+
private prune;
|
|
65
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { InteractivitySnapshot, SessionMode } from '../analytics/interactivity-snapshot';
|
|
2
|
+
export type InteractivityCollectorOptions = {
|
|
3
|
+
emit: (snapshot: InteractivitySnapshot) => void;
|
|
4
|
+
/** Elements inside the editor's content DOM. Read at snapshot time. */
|
|
5
|
+
getEditorDomSize: () => number | undefined;
|
|
6
|
+
/** Size of the ProseMirror document. Read at snapshot time. */
|
|
7
|
+
getNodeSize: () => number | undefined;
|
|
8
|
+
getObjectId: () => string | undefined;
|
|
9
|
+
getSessionMode: () => SessionMode | undefined;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Collects interaction latencies for one editor mount and emits session-to-date snapshots.
|
|
13
|
+
*
|
|
14
|
+
* Every interaction is counted in `page`, and the ones with the editor again in one of the editor
|
|
15
|
+
* groups, which is what tells a slow editor apart from a slow page around it.
|
|
16
|
+
*
|
|
17
|
+
* `start` and `stop` bound the collecting, which happens once. Within it there can be several
|
|
18
|
+
* sessions, because a session covers one document in one mode: a Confluence live page
|
|
19
|
+
* navigates and switches between reading and editing without ever remounting the editor, and
|
|
20
|
+
* each of those closes the current session and opens the next.
|
|
21
|
+
*/
|
|
22
|
+
export declare class InteractivityCollector {
|
|
23
|
+
private readonly emit;
|
|
24
|
+
private readonly getEditorDomSize;
|
|
25
|
+
private readonly getNodeSize;
|
|
26
|
+
private readonly getObjectId;
|
|
27
|
+
private readonly getSessionMode;
|
|
28
|
+
private readonly interactionObserver;
|
|
29
|
+
private readonly editorEvents;
|
|
30
|
+
private readonly snapshotScheduler;
|
|
31
|
+
private readonly lifecycleObserver;
|
|
32
|
+
private session;
|
|
33
|
+
private editorRoot;
|
|
34
|
+
private started;
|
|
35
|
+
private stopped;
|
|
36
|
+
constructor(options: InteractivityCollectorOptions);
|
|
37
|
+
/**
|
|
38
|
+
* Starts collecting. Calling it again while collecting changes nothing.
|
|
39
|
+
*
|
|
40
|
+
* @returns whether collecting is running; `false` when the browser cannot support it, or
|
|
41
|
+
* when it has already been stopped.
|
|
42
|
+
*/
|
|
43
|
+
start(): boolean;
|
|
44
|
+
/** Reports the session as ended by the editor unmounting, and stops collecting. */
|
|
45
|
+
stop(): void;
|
|
46
|
+
/**
|
|
47
|
+
* The element the editor renders itself into is what makes an interaction one of the editor's.
|
|
48
|
+
* The session is not tied to it: interactions from before it arrives are counted in `page`.
|
|
49
|
+
*/
|
|
50
|
+
setEditorRoot(root: Element | null | undefined): void;
|
|
51
|
+
/**
|
|
52
|
+
* Rotates the session when the editor is pointed at different content.
|
|
53
|
+
*
|
|
54
|
+
* An unknown object id is no information, never a change. The provider resolves
|
|
55
|
+
* asynchronously after mount, and `contextIdentifierPlugin` resets its state to the
|
|
56
|
+
* configured provider on transactions that do not carry a new one, so the id read here goes
|
|
57
|
+
* missing for a moment on a document that never changed.
|
|
58
|
+
*/
|
|
59
|
+
onObjectIdChanged(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Rotates the session when the editor switches between reading and editing. Interactions
|
|
62
|
+
* with a read-only page are a different population from interactions while editing, so one
|
|
63
|
+
* session never covers both.
|
|
64
|
+
*/
|
|
65
|
+
onViewModeChanged(): void;
|
|
66
|
+
/**
|
|
67
|
+
* @param startsAfterInteractionId the highest interaction the previous session saw, so
|
|
68
|
+
* entries still arriving for it are not counted here as well.
|
|
69
|
+
*/
|
|
70
|
+
private createSession;
|
|
71
|
+
/** Reports the current session as ended by `reason` and puts a new one in its place. */
|
|
72
|
+
private rotateSession;
|
|
73
|
+
private onTimer;
|
|
74
|
+
private onHidden;
|
|
75
|
+
private onVisible;
|
|
76
|
+
private onPageHide;
|
|
77
|
+
/**
|
|
78
|
+
* The page came back from the back/forward cache, which never raises a visibility change.
|
|
79
|
+
* The session continues, and the signal that suspended it has been reported, so the next
|
|
80
|
+
* one is due.
|
|
81
|
+
*/
|
|
82
|
+
private onPageShow;
|
|
83
|
+
/**
|
|
84
|
+
* Counts an interaction with the editor, including the ones Event Timing never reports because
|
|
85
|
+
* they were faster than its threshold — which is why a count alone moves the session on.
|
|
86
|
+
*/
|
|
87
|
+
private recordEditorEvent;
|
|
88
|
+
private recordEntries;
|
|
89
|
+
private takeSnapshot;
|
|
90
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { SessionMode } from '../analytics/interactivity-snapshot';
|
|
2
|
+
import { InteractionTracker } from './interaction-tracker';
|
|
3
|
+
import { InteractionGroup } from './interaction-group';
|
|
4
|
+
export type InteractivitySessionStart = {
|
|
5
|
+
hidden: boolean;
|
|
6
|
+
mode: SessionMode | undefined;
|
|
7
|
+
objectId: string | undefined;
|
|
8
|
+
startsAfterInteractionId: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Everything that belongs to one session.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here is reset — the next session is a new instance — so a field added here cannot
|
|
14
|
+
* be left carrying the previous session's value.
|
|
15
|
+
*/
|
|
16
|
+
export declare class InteractivitySession {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly startedAt: number;
|
|
19
|
+
/** Page interaction count when the session opened, subtracted to get its own total. */
|
|
20
|
+
readonly interactionCountAtStart: number;
|
|
21
|
+
readonly tracker: InteractionTracker;
|
|
22
|
+
readonly page: InteractionGroup;
|
|
23
|
+
readonly editorTyping: InteractionGroup;
|
|
24
|
+
readonly editorPointer: InteractionGroup;
|
|
25
|
+
readonly editorOther: InteractionGroup;
|
|
26
|
+
/** Increments per snapshot; a query takes the highest one per session. */
|
|
27
|
+
seq: number;
|
|
28
|
+
/** Both fixed for the session: a change to either closes it and opens the next. */
|
|
29
|
+
objectId: string | undefined;
|
|
30
|
+
mode: SessionMode | undefined;
|
|
31
|
+
hiddenMs: number;
|
|
32
|
+
hiddenSince: number | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Bumped on every change to the accumulated data. A snapshot is emitted only when this
|
|
35
|
+
* has moved past `emittedRevision`, so identical snapshots are never sent twice.
|
|
36
|
+
*/
|
|
37
|
+
revision: number;
|
|
38
|
+
emittedRevision: number;
|
|
39
|
+
/** One lifecycle snapshot per hidden episode; cleared when the page comes back. */
|
|
40
|
+
lifecycleSnapshotEmitted: boolean;
|
|
41
|
+
constructor(start: InteractivitySessionStart);
|
|
42
|
+
}
|