@atlaskit/editor-plugin-interactivity 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +34 -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/interaction-group.js +167 -0
- package/dist/cjs/collector/interaction-observer.js +114 -0
- package/dist/cjs/collector/interaction-tracker.js +144 -0
- package/dist/cjs/collector/interactivity-collector.js +289 -0
- package/dist/cjs/collector/interactivity-session.js +52 -0
- package/dist/cjs/collector/lifecycle-observer.js +66 -0
- package/dist/cjs/collector/snapshot-scheduler.js +70 -0
- package/dist/cjs/interactivityPlugin.js +81 -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/interaction-group.js +110 -0
- package/dist/es2019/collector/interaction-observer.js +90 -0
- package/dist/es2019/collector/interaction-tracker.js +116 -0
- package/dist/es2019/collector/interactivity-collector.js +228 -0
- package/dist/es2019/collector/interactivity-session.js +42 -0
- package/dist/es2019/collector/lifecycle-observer.js +46 -0
- package/dist/es2019/collector/snapshot-scheduler.js +48 -0
- package/dist/es2019/interactivityPlugin.js +75 -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/interaction-group.js +161 -0
- package/dist/esm/collector/interaction-observer.js +108 -0
- package/dist/esm/collector/interaction-tracker.js +137 -0
- package/dist/esm/collector/interactivity-collector.js +282 -0
- package/dist/esm/collector/interactivity-session.js +45 -0
- package/dist/esm/collector/lifecycle-observer.js +59 -0
- package/dist/esm/collector/snapshot-scheduler.js +63 -0
- package/dist/esm/interactivityPlugin.js +81 -6
- package/dist/types/analytics/fire-interactivity-event.d.ts +9 -0
- package/dist/types/analytics/interactivity-snapshot.d.ts +61 -0
- package/dist/types/collector/bucket-boundaries.d.ts +35 -0
- package/dist/types/collector/interaction-group.d.ts +25 -0
- package/dist/types/collector/interaction-observer.d.ts +39 -0
- package/dist/types/collector/interaction-tracker.d.ts +56 -0
- package/dist/types/collector/interactivity-collector.d.ts +75 -0
- package/dist/types/collector/interactivity-session.d.ts +39 -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 +25 -7
- package/package.json +8 -3
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import _createClass from "@babel/runtime/helpers/createClass";
|
|
2
|
+
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
|
|
3
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
4
|
+
import { InteractionObserver } from './interaction-observer';
|
|
5
|
+
import { InteractionTracker } from './interaction-tracker';
|
|
6
|
+
import { InteractionGroup } from './interaction-group';
|
|
7
|
+
function createSessionId() {
|
|
8
|
+
if (typeof crypto.randomUUID === 'function') {
|
|
9
|
+
return crypto.randomUUID();
|
|
10
|
+
}
|
|
11
|
+
var bytes = new Uint8Array(16);
|
|
12
|
+
crypto.getRandomValues(bytes);
|
|
13
|
+
return Array.from(bytes, function (byte) {
|
|
14
|
+
return byte.toString(16).padStart(2, '0');
|
|
15
|
+
}).join('');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Everything that belongs to one session.
|
|
19
|
+
*
|
|
20
|
+
* Nothing here is reset — the next session is a new instance — so a field added here cannot
|
|
21
|
+
* be left carrying the previous session's value.
|
|
22
|
+
*/
|
|
23
|
+
export var InteractivitySession = /*#__PURE__*/_createClass(function InteractivitySession(start) {
|
|
24
|
+
_classCallCheck(this, InteractivitySession);
|
|
25
|
+
_defineProperty(this, "id", createSessionId());
|
|
26
|
+
_defineProperty(this, "startedAt", performance.now());
|
|
27
|
+
/** Page interaction count when the session opened, subtracted to get its own total. */
|
|
28
|
+
_defineProperty(this, "interactionCountAtStart", InteractionObserver.readPageInteractionCount());
|
|
29
|
+
_defineProperty(this, "page", new InteractionGroup());
|
|
30
|
+
/** Increments per snapshot; a query takes the highest one per session. */
|
|
31
|
+
_defineProperty(this, "seq", 0);
|
|
32
|
+
_defineProperty(this, "hiddenMs", 0);
|
|
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
|
+
_defineProperty(this, "revision", 0);
|
|
38
|
+
_defineProperty(this, "emittedRevision", 0);
|
|
39
|
+
/** One lifecycle snapshot per hidden episode; cleared when the page comes back. */
|
|
40
|
+
_defineProperty(this, "lifecycleSnapshotEmitted", false);
|
|
41
|
+
this.objectId = start.objectId;
|
|
42
|
+
this.mode = start.mode;
|
|
43
|
+
this.hiddenSince = start.hidden ? this.startedAt : undefined;
|
|
44
|
+
this.tracker = new InteractionTracker(start.startsAfterInteractionId);
|
|
45
|
+
});
|
|
@@ -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,87 @@
|
|
|
1
|
+
import { useEffect } 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
|
+
useEffect(function () {
|
|
21
|
+
// Nothing measured here is worth an editor. This effect runs inside the plugin slot's
|
|
22
|
+
// error boundary, which also renders every plugin's content components, so a throw
|
|
23
|
+
// from instrumentation would take that UI down with it.
|
|
24
|
+
try {
|
|
25
|
+
var _api$contextIdentifie2, _api$editorViewMode2;
|
|
26
|
+
var collector = new InteractivityCollector({
|
|
27
|
+
emit: function emit(snapshot) {
|
|
28
|
+
var _api$analytics;
|
|
29
|
+
return fireInteractivityEvent(api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions, snapshot);
|
|
30
|
+
},
|
|
31
|
+
getObjectId: function getObjectId() {
|
|
32
|
+
var _api$contextIdentifie;
|
|
33
|
+
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;
|
|
34
|
+
},
|
|
35
|
+
getSessionMode: function getSessionMode() {
|
|
36
|
+
var _api$editorViewMode;
|
|
37
|
+
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;
|
|
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: function getNodeSize() {
|
|
46
|
+
return editorView.isDestroyed ? undefined : editorView.state.doc.nodeSize;
|
|
47
|
+
},
|
|
48
|
+
// `getElementsByTagName` counts descendants in the browser engine, so this
|
|
49
|
+
// cannot overflow the stack on very large documents.
|
|
50
|
+
getEditorDomSize: function getEditorDomSize() {
|
|
51
|
+
return editorView.isDestroyed ? undefined : editorView.dom.getElementsByTagName('*').length;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
if (!collector.start()) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Confluence live pages navigate and switch between reading and editing without
|
|
59
|
+
// remounting the editor. A session covers one document in one mode, so either
|
|
60
|
+
// change ends it and starts the next.
|
|
61
|
+
var unsubscribeFromObjectId = api === null || api === void 0 || (_api$contextIdentifie2 = api.contextIdentifier) === null || _api$contextIdentifie2 === void 0 ? void 0 : _api$contextIdentifie2.sharedState.onChange(function () {
|
|
62
|
+
return collector.onObjectIdChanged();
|
|
63
|
+
});
|
|
64
|
+
var unsubscribeFromViewMode = api === null || api === void 0 || (_api$editorViewMode2 = api.editorViewMode) === null || _api$editorViewMode2 === void 0 ? void 0 : _api$editorViewMode2.sharedState.onChange(function () {
|
|
65
|
+
return collector.onViewModeChanged();
|
|
66
|
+
});
|
|
67
|
+
return function () {
|
|
68
|
+
unsubscribeFromObjectId === null || unsubscribeFromObjectId === void 0 || unsubscribeFromObjectId();
|
|
69
|
+
unsubscribeFromViewMode === null || unsubscribeFromViewMode === void 0 || unsubscribeFromViewMode();
|
|
70
|
+
collector.stop();
|
|
71
|
+
};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// Instrumentation must not fail the editor around it, but a failure that hits every
|
|
74
|
+
// browser of one engine would be invisible without this.
|
|
75
|
+
void logException(error, {
|
|
76
|
+
location: 'editor-plugin-interactivity/start'
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// `api` is deliberately not a dependency: a preset reconfigure hands out a new
|
|
81
|
+
// proxy object, but the one captured here keeps resolving plugins from the same
|
|
82
|
+
// live registry, and restarting the session on a reconfigure would split one
|
|
83
|
+
// editor session in two.
|
|
84
|
+
}, [editorView]);
|
|
85
|
+
}
|
|
11
86
|
};
|
|
12
87
|
};
|
|
@@ -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,61 @@
|
|
|
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
|
+
hiddenMs: number;
|
|
52
|
+
interactivitySessionId: string;
|
|
53
|
+
nodeSize?: number;
|
|
54
|
+
objectId?: string;
|
|
55
|
+
page: InteractionGroupSnapshot;
|
|
56
|
+
reason: SnapshotReason;
|
|
57
|
+
schema: number;
|
|
58
|
+
seq: number;
|
|
59
|
+
/** Fixed for the whole session: a mode change closes it and opens the next. */
|
|
60
|
+
sessionMode?: SessionMode;
|
|
61
|
+
};
|
|
@@ -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,25 @@
|
|
|
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
|
+
export declare class InteractionGroup {
|
|
12
|
+
private countByLatency;
|
|
13
|
+
add(latencyMs: number): void;
|
|
14
|
+
remeasure(previousLatencyMs: number, latencyMs: number): void;
|
|
15
|
+
/**
|
|
16
|
+
* @param totalCount every interaction of the group, including those below the Event Timing
|
|
17
|
+
* reporting threshold, which this group never sees.
|
|
18
|
+
*/
|
|
19
|
+
snapshot(totalCount: number): InteractionGroupSnapshot;
|
|
20
|
+
private observedCount;
|
|
21
|
+
/** Rounding lives here so that every count goes through the same step, in or out. */
|
|
22
|
+
private roundLatencyUp;
|
|
23
|
+
private increment;
|
|
24
|
+
private decrement;
|
|
25
|
+
}
|
|
@@ -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,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Event Timing fields this package reads. `interactionId` is missing from the DOM
|
|
3
|
+
* typings' `PerformanceEventTiming`, so it is declared here as optional, which also makes
|
|
4
|
+
* a `PerformanceEntry` from `getEntries()` assignable without a cast.
|
|
5
|
+
*/
|
|
6
|
+
export type InteractionEntry = PerformanceEntry & {
|
|
7
|
+
interactionId?: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* What an entry did to the interaction it belongs to: either it is the first entry of a new
|
|
11
|
+
* interaction, or it measured an interaction that was already counted as slower than it was
|
|
12
|
+
* known to be.
|
|
13
|
+
*/
|
|
14
|
+
export type InteractionUpdate = {
|
|
15
|
+
latencyMs: number;
|
|
16
|
+
type: 'new';
|
|
17
|
+
} | {
|
|
18
|
+
fromMs: number;
|
|
19
|
+
toMs: number;
|
|
20
|
+
type: 'remeasured';
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Groups Event Timing entries into interactions.
|
|
24
|
+
*
|
|
25
|
+
* Entries sharing a non-zero `interactionId` are one interaction whose latency is the
|
|
26
|
+
* maximum `duration` among them. Entries arrive incrementally, so an interaction's latency
|
|
27
|
+
* can grow after it was first reported — callers apply that to what they already counted
|
|
28
|
+
* rather than counting the interaction twice.
|
|
29
|
+
*
|
|
30
|
+
* A non-zero `interactionId` is the browser's own definition of an interaction, which is
|
|
31
|
+
* also what INP filters on: it is assigned to the pointer and keyboard events that make one
|
|
32
|
+
* up, and never to scrolling or pointer movement.
|
|
33
|
+
*/
|
|
34
|
+
export declare class InteractionTracker {
|
|
35
|
+
private latencies;
|
|
36
|
+
private readonly startsAfterInteractionId;
|
|
37
|
+
private highestInteractionId;
|
|
38
|
+
/**
|
|
39
|
+
* @param startsAfterInteractionId interactions up to and including this one belong to the
|
|
40
|
+
* previous tracker and are ignored. `interactionId` counts up over the life of the page, so
|
|
41
|
+
* a session opening mid-page passes the highest id the one before it saw; without that, an
|
|
42
|
+
* entry still arriving for an interaction from the previous session would look new here and
|
|
43
|
+
* be counted in both.
|
|
44
|
+
*/
|
|
45
|
+
constructor(startsAfterInteractionId?: number);
|
|
46
|
+
/** The highest `interactionId` this tracker has seen. */
|
|
47
|
+
get lastInteractionId(): number;
|
|
48
|
+
/**
|
|
49
|
+
* Merges an entry into the interaction it belongs to.
|
|
50
|
+
*
|
|
51
|
+
* @returns what that did to the interaction's latency, or nothing when the entry is not
|
|
52
|
+
* part of an interaction, belongs to a previous tracker, or does not change one.
|
|
53
|
+
*/
|
|
54
|
+
merge(entry: InteractionEntry): InteractionUpdate | undefined;
|
|
55
|
+
private prune;
|
|
56
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
* `start` and `stop` bound the collecting, which happens once. Within it there can be several
|
|
15
|
+
* sessions, because a session covers one document in one mode: a Confluence live page
|
|
16
|
+
* navigates and switches between reading and editing without ever remounting the editor, and
|
|
17
|
+
* each of those closes the current session and opens the next.
|
|
18
|
+
*/
|
|
19
|
+
export declare class InteractivityCollector {
|
|
20
|
+
private readonly emit;
|
|
21
|
+
private readonly getEditorDomSize;
|
|
22
|
+
private readonly getNodeSize;
|
|
23
|
+
private readonly getObjectId;
|
|
24
|
+
private readonly getSessionMode;
|
|
25
|
+
private readonly interactionObserver;
|
|
26
|
+
private readonly snapshotScheduler;
|
|
27
|
+
private readonly lifecycleObserver;
|
|
28
|
+
private session;
|
|
29
|
+
private started;
|
|
30
|
+
private stopped;
|
|
31
|
+
constructor(options: InteractivityCollectorOptions);
|
|
32
|
+
/**
|
|
33
|
+
* Starts collecting. Calling it again while collecting changes nothing.
|
|
34
|
+
*
|
|
35
|
+
* @returns whether collecting is running; `false` when the browser cannot support it, or
|
|
36
|
+
* when it has already been stopped.
|
|
37
|
+
*/
|
|
38
|
+
start(): boolean;
|
|
39
|
+
/** Reports the session as ended by the editor unmounting, and stops collecting. */
|
|
40
|
+
stop(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Rotates the session when the editor is pointed at different content.
|
|
43
|
+
*
|
|
44
|
+
* An unknown object id is no information, never a change. The provider resolves
|
|
45
|
+
* asynchronously after mount, and `contextIdentifierPlugin` resets its state to the
|
|
46
|
+
* configured provider on transactions that do not carry a new one, so the id read here goes
|
|
47
|
+
* missing for a moment on a document that never changed.
|
|
48
|
+
*/
|
|
49
|
+
onObjectIdChanged(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Rotates the session when the editor switches between reading and editing. Interactions
|
|
52
|
+
* with a read-only page are a different population from interactions while editing, so one
|
|
53
|
+
* session never covers both.
|
|
54
|
+
*/
|
|
55
|
+
onViewModeChanged(): void;
|
|
56
|
+
/**
|
|
57
|
+
* @param startsAfterInteractionId the highest interaction the previous session saw, so
|
|
58
|
+
* entries still arriving for it are not counted here as well.
|
|
59
|
+
*/
|
|
60
|
+
private createSession;
|
|
61
|
+
/** Reports the current session as ended by `reason` and puts a new one in its place. */
|
|
62
|
+
private rotateSession;
|
|
63
|
+
private onTimer;
|
|
64
|
+
private onHidden;
|
|
65
|
+
private onVisible;
|
|
66
|
+
private onPageHide;
|
|
67
|
+
/**
|
|
68
|
+
* The page came back from the back/forward cache, which never raises a visibility change.
|
|
69
|
+
* The session continues, and the signal that suspended it has been reported, so the next
|
|
70
|
+
* one is due.
|
|
71
|
+
*/
|
|
72
|
+
private onPageShow;
|
|
73
|
+
private recordEntries;
|
|
74
|
+
private takeSnapshot;
|
|
75
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
/** Increments per snapshot; a query takes the highest one per session. */
|
|
24
|
+
seq: number;
|
|
25
|
+
/** Both fixed for the session: a change to either closes it and opens the next. */
|
|
26
|
+
objectId: string | undefined;
|
|
27
|
+
mode: SessionMode | undefined;
|
|
28
|
+
hiddenMs: number;
|
|
29
|
+
hiddenSince: number | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Bumped on every change to the accumulated data. A snapshot is emitted only when this
|
|
32
|
+
* has moved past `emittedRevision`, so identical snapshots are never sent twice.
|
|
33
|
+
*/
|
|
34
|
+
revision: number;
|
|
35
|
+
emittedRevision: number;
|
|
36
|
+
/** One lifecycle snapshot per hidden episode; cleared when the page comes back. */
|
|
37
|
+
lifecycleSnapshotEmitted: boolean;
|
|
38
|
+
constructor(start: InteractivitySessionStart);
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type LifecycleHandlers = {
|
|
2
|
+
/** The tab became hidden. */
|
|
3
|
+
onHidden: () => void;
|
|
4
|
+
/** The page is being unloaded. May arrive right after `onHidden`. */
|
|
5
|
+
onPageHide: () => void;
|
|
6
|
+
/** The page came back from the back/forward cache. */
|
|
7
|
+
onPageShow: () => void;
|
|
8
|
+
/** The tab became visible again — the session continues. */
|
|
9
|
+
onVisible: () => void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Reports the page lifecycle signals that force an extra snapshot: the tab being hidden or
|
|
13
|
+
* shown again, and the page being unloaded or restored from the back/forward cache.
|
|
14
|
+
*
|
|
15
|
+
* None of them is guaranteed to arrive — a mobile browser being killed raises nothing at all.
|
|
16
|
+
* Snapshots carry session-to-date values, so losing the last one costs only the tail of that
|
|
17
|
+
* session.
|
|
18
|
+
*/
|
|
19
|
+
export declare class LifecycleObserver {
|
|
20
|
+
private readonly handlers;
|
|
21
|
+
private unbind;
|
|
22
|
+
constructor(handlers: LifecycleHandlers);
|
|
23
|
+
start(): void;
|
|
24
|
+
stop(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fires `onTick` on the snapshot cadence. Timings are offsets from `start()`, so a
|
|
3
|
+
* restart (a new session in the same editor) restarts the cadence too.
|
|
4
|
+
*/
|
|
5
|
+
export declare class SnapshotScheduler {
|
|
6
|
+
private readonly onTick;
|
|
7
|
+
private timeoutId;
|
|
8
|
+
private tickIndex;
|
|
9
|
+
constructor(onTick: () => void);
|
|
10
|
+
start(): void;
|
|
11
|
+
restart(): void;
|
|
12
|
+
stop(): void;
|
|
13
|
+
private scheduleNext;
|
|
14
|
+
private delayForNextTick;
|
|
15
|
+
}
|