@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-interactivity
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`fc88b1544be96`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/fc88b1544be96) -
|
|
8
|
+
Report the `editor interactivity` operational event: how interaction latencies were spread over an
|
|
9
|
+
editor session, as bucketed counts, for the Confluence full page editor. Behind the
|
|
10
|
+
platform_editor_editor_interactivity experiment.
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
|
|
3
16
|
## 0.1.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -7,14 +7,42 @@ be able to use this component but will not be able to submit issues.
|
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
9
9
|
|
|
10
|
-
The Interactivity plugin reports the `editor interactivity` operational event:
|
|
11
|
-
|
|
10
|
+
The Interactivity plugin reports the `editor interactivity` operational event: session-to-date
|
|
11
|
+
interaction latency distributions for full page editor sessions, per
|
|
12
12
|
[RFC 095](https://hello.atlassian.net/wiki/spaces/EDITOR/pages/7527607488/Editor+RFC+095+Confluence+editor+responsiveness+bucketed+INP+telemetry).
|
|
13
13
|
The existing `editor inp` event reports a single value per session and cannot answer how many
|
|
14
|
-
interactions were slow.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
interactions were slow, so this plugin keeps bucketed counts instead.
|
|
15
|
+
|
|
16
|
+
## What it reports
|
|
17
|
+
|
|
18
|
+
- `page` — every interaction on the page, as `totalCount`, `observedCount`, `sumMs`, `maxMs`,
|
|
19
|
+
`buckets` and `percentilesMs`. `totalCount` comes from `performance.interactionCount` and includes
|
|
20
|
+
interactions below the 16 ms Event Timing reporting threshold, so `totalCount - observedCount` is
|
|
21
|
+
the sub-threshold count.
|
|
22
|
+
- `percentilesMs` is temporary. It holds percentiles of the same interactions, keyed by percentile
|
|
23
|
+
and exact to the 8 ms Event Timing reports durations at, to confirm that a percentile read off
|
|
24
|
+
`buckets` lands where the latencies actually are.
|
|
25
|
+
- Bucket keys are the upper boundary of the bucket in milliseconds and count only interactions above
|
|
26
|
+
the previous boundary. Buckets are not cumulative and empty buckets are omitted, so a missing
|
|
27
|
+
bucket means zero. The boundaries are versioned by `schema`.
|
|
28
|
+
- Snapshots are session-to-date, so a cohort query takes the highest `seq` per
|
|
29
|
+
`interactivitySessionId` and then sums bucket counts.
|
|
30
|
+
|
|
31
|
+
## Cadence
|
|
32
|
+
|
|
33
|
+
Snapshots are taken 10 s, 30 s and 60 s after the session starts, then every 60 s, plus on every
|
|
34
|
+
transition of the tab to hidden, on `pagehide` and on editor unmount. A snapshot that would repeat
|
|
35
|
+
the previous one is skipped, and signals from one lifecycle transition are coalesced into a single
|
|
36
|
+
snapshot.
|
|
37
|
+
|
|
38
|
+
## Sessions
|
|
39
|
+
|
|
40
|
+
A session covers one document in one mode, so its latencies are always comparable with each other.
|
|
41
|
+
It normally lasts an editor mount, but it also ends while the editor stays mounted when either of
|
|
42
|
+
those changes — Confluence live pages navigate and switch between reading and editing without
|
|
43
|
+
remounting the editor. That is reported as `reason: 'navigation'` or `reason: 'modeChange'`, and the
|
|
44
|
+
next session starts with a new `interactivitySessionId`. Sessions of one editor mount share an
|
|
45
|
+
`editorSessionId`, which is what stitches them back together.
|
|
18
46
|
|
|
19
47
|
## Install
|
|
20
48
|
|
package/afm-cc/tsconfig.json
CHANGED
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
"noCheck": true
|
|
19
19
|
},
|
|
20
20
|
"references": [
|
|
21
|
+
{
|
|
22
|
+
"path": "../../../helpers/browser-apis/afm-cc/tsconfig.json"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "../../editor-plugin-analytics/afm-cc/tsconfig.json"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "../../editor-plugin-context-identifier/afm-cc/tsconfig.json"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "../../editor-plugin-editor-viewmode/afm-cc/tsconfig.json"
|
|
32
|
+
},
|
|
21
33
|
{
|
|
22
34
|
"path": "../../editor-common/afm-cc/tsconfig.json"
|
|
23
35
|
}
|
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
"noCheck": true
|
|
19
19
|
},
|
|
20
20
|
"references": [
|
|
21
|
+
{
|
|
22
|
+
"path": "../../../helpers/browser-apis/afm-products/tsconfig.json"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "../../editor-plugin-analytics/afm-products/tsconfig.json"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "../../editor-plugin-context-identifier/afm-products/tsconfig.json"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "../../editor-plugin-editor-viewmode/afm-products/tsconfig.json"
|
|
32
|
+
},
|
|
21
33
|
{
|
|
22
34
|
"path": "../../editor-common/afm-products/tsconfig.json"
|
|
23
35
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.fireInteractivityEvent = fireInteractivityEvent;
|
|
7
|
+
var _enums = require("@atlaskit/editor-common/analytics/types/enums");
|
|
8
|
+
/** The reasons that mean the page may not be around long enough to deliver a queued event. */
|
|
9
|
+
var REASONS_THE_PAGE_MAY_NOT_OUTLIVE = ['hidden', 'pagehide', 'unmount'];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The `editor interactivity` operational event.
|
|
13
|
+
*
|
|
14
|
+
* `editorSessionId` arrives without being set here: `@atlaskit/analytics-listeners` merges
|
|
15
|
+
* the editor analytics context into the attributes of every event on the editor channel.
|
|
16
|
+
* `objectId` comes both from that context and from the snapshot, which carries the value
|
|
17
|
+
* the session was collected against.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sends a snapshot as the `editor interactivity` event.
|
|
22
|
+
*
|
|
23
|
+
* Does nothing without the analytics plugin: an editor without it has nowhere to send events,
|
|
24
|
+
* and the collector keeps measuring either way.
|
|
25
|
+
*/
|
|
26
|
+
function fireInteractivityEvent(analytics, snapshot) {
|
|
27
|
+
analytics === null || analytics === void 0 || analytics.fireAnalyticsEvent({
|
|
28
|
+
action: 'interactivity',
|
|
29
|
+
actionSubject: _enums.ACTION_SUBJECT.EDITOR,
|
|
30
|
+
eventType: _enums.EVENT_TYPE.OPERATIONAL,
|
|
31
|
+
attributes: snapshot
|
|
32
|
+
}, undefined,
|
|
33
|
+
// Events are queued into an idle callback by default, which a page being unloaded never
|
|
34
|
+
// runs and a backgrounded tab throttles. The snapshots taken because the page is going
|
|
35
|
+
// away go immediately; the rest can wait their turn.
|
|
36
|
+
{
|
|
37
|
+
immediate: REASONS_THE_PAGE_MAY_NOT_OUTLIVE.includes(snapshot.reason)
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.SCHEMA_VERSION = exports.REPORTING_THRESHOLD_MS = void 0;
|
|
7
|
+
exports.bucketKeyForMs = bucketKeyForMs;
|
|
8
|
+
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; } } }; }
|
|
9
|
+
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; } }
|
|
10
|
+
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; }
|
|
11
|
+
/**
|
|
12
|
+
* Bucket boundaries for the interaction latency buckets, version 1.
|
|
13
|
+
*
|
|
14
|
+
* Two ranges:
|
|
15
|
+
* - 16 ms to 200 ms — a boundary every 8 ms: 16, 24, 32, … 200. Event Timing rounds
|
|
16
|
+
* durations to 8 ms, so nothing finer is measurable.
|
|
17
|
+
* - above 200 ms — each boundary ~15% above the previous one, five of them per doubling:
|
|
18
|
+
* 222, 256, 294, 337, 388, 445, 512, … A 40 ms difference matters at 100 ms and is
|
|
19
|
+
* noise at 4 seconds, so buckets grow with the latency instead of staying 8 ms wide.
|
|
20
|
+
*
|
|
21
|
+
* 500 ms — the Google INP "poor" threshold — falls inside the 445…512 bucket, so that one
|
|
22
|
+
* bucket is split at 500 to count the threshold instead of interpolating it. 200 ms, the
|
|
23
|
+
* "good" threshold, is already a boundary.
|
|
24
|
+
*
|
|
25
|
+
* Bump SCHEMA_VERSION whenever any boundary moves; queries group by it.
|
|
26
|
+
*/
|
|
27
|
+
var SCHEMA_VERSION = exports.SCHEMA_VERSION = 1;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Event Timing reporting threshold. Faster interactions are never delivered to the
|
|
31
|
+
* observer, so they reach no bucket at all — `performance.interactionCount` is what counts
|
|
32
|
+
* them, as `totalCount - observedCount`. This bucket holds the interactions reported at
|
|
33
|
+
* exactly the threshold.
|
|
34
|
+
*/
|
|
35
|
+
var REPORTING_THRESHOLD_MS = exports.REPORTING_THRESHOLD_MS = 16;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Exceptions to the grid: latencies that must be a boundary of their own so that they are
|
|
39
|
+
* counted exactly rather than read off a bucket that spans them. Each one splits the bucket it
|
|
40
|
+
* falls inside. 500 ms is the Google INP "poor" threshold; the "good" one, 200 ms, needs no
|
|
41
|
+
* exception because the evenly spaced range already ends there.
|
|
42
|
+
*
|
|
43
|
+
* Every entry has to sit above that range, and adding one changes the reported keys, so bump
|
|
44
|
+
* SCHEMA_VERSION with it.
|
|
45
|
+
*/
|
|
46
|
+
var EXACT_THRESHOLDS_MS = [500];
|
|
47
|
+
var EVENLY_SPACED_MAX_MS = 200;
|
|
48
|
+
/**
|
|
49
|
+
* Keep this a multiple of 8 (8, 16, 24, …). Event Timing reports durations in 8 ms steps, so a
|
|
50
|
+
* step that is not a multiple of 8 leaves buckets no interaction can ever land in.
|
|
51
|
+
*/
|
|
52
|
+
var EVENLY_SPACED_STEP_MS = 8;
|
|
53
|
+
var BOUNDARIES_PER_DOUBLING = 5;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The first boundary at or above `latencyMs`, for the range above 200 ms.
|
|
57
|
+
*
|
|
58
|
+
* Five boundaries per doubling is the same as saying the nth boundary sits at `2^(n/5)` ms —
|
|
59
|
+
* boundary 40 at 256 ms, 45 at 512 ms, 50 at 1024 ms. So this turns the latency into a
|
|
60
|
+
* boundary number, rounds that up, and turns it back into milliseconds.
|
|
61
|
+
*
|
|
62
|
+
* Boundaries are floored to whole milliseconds, which keeps each one at or below the exact
|
|
63
|
+
* value it stands for. That is what makes `bucketKeyForMs(boundary) === boundary` hold.
|
|
64
|
+
*/
|
|
65
|
+
function firstBoundaryAtOrAbove(latencyMs) {
|
|
66
|
+
// `Math.log2(latencyMs) * 5` is the boundary number: log2 answers how many doublings of
|
|
67
|
+
// 1 ms reach this latency, and five boundaries cover each doubling.
|
|
68
|
+
//
|
|
69
|
+
// A latency sitting on a boundary makes that a whole number, which `Math.ceil` has to
|
|
70
|
+
// keep. ECMA-262 only requires `Math.log2` to be approximate, so 9.0000000000000002 for
|
|
71
|
+
// `log2(512)` would round up to boundary 46 and report 512 ms as 588 ms. EPSILON is
|
|
72
|
+
// larger than such imprecision and far smaller than the gap between two boundaries.
|
|
73
|
+
var EPSILON = 1e-9;
|
|
74
|
+
var boundaryNumber = Math.ceil(BOUNDARIES_PER_DOUBLING * Math.log2(latencyMs) - EPSILON);
|
|
75
|
+
|
|
76
|
+
// One `Math.pow` over the whole exponent, so the result is rounded once. Multiplying the
|
|
77
|
+
// ratio between neighbouring boundaries (2^(1/5) ≈ 1.1487) by itself instead accumulates
|
|
78
|
+
// the rounding of every step, reaching 512.0000000000018 by boundary 45.
|
|
79
|
+
return Math.floor(Math.pow(2, boundaryNumber / BOUNDARIES_PER_DOUBLING));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The bucket each exact threshold splits — 500 ms splits the one ending at 512 ms. */
|
|
83
|
+
var EXACT_THRESHOLD_BUCKETS = EXACT_THRESHOLDS_MS.map(function (thresholdMs) {
|
|
84
|
+
return {
|
|
85
|
+
thresholdMs: thresholdMs,
|
|
86
|
+
bucketMs: firstBoundaryAtOrAbove(thresholdMs)
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The bucket a latency belongs to, identified by the bucket's upper boundary in whole
|
|
92
|
+
* milliseconds — which is also its key in the reported buckets.
|
|
93
|
+
*
|
|
94
|
+
* Every latency gets a bucket, however slow: the boundaries continue upwards, so there is no
|
|
95
|
+
* overflow bucket. Expects the whole-millisecond durations Event Timing reports; boundaries
|
|
96
|
+
* are floored, so a fractional latency can land in a bucket whose key reads up to a
|
|
97
|
+
* millisecond below it. Non-finite latencies never reach here — `InteractionTracker` drops
|
|
98
|
+
* them as it reads the entry.
|
|
99
|
+
*/
|
|
100
|
+
function bucketKeyForMs(latencyMs) {
|
|
101
|
+
if (latencyMs <= REPORTING_THRESHOLD_MS) {
|
|
102
|
+
return REPORTING_THRESHOLD_MS;
|
|
103
|
+
}
|
|
104
|
+
if (latencyMs <= EVENLY_SPACED_MAX_MS) {
|
|
105
|
+
// How many 8 ms steps above the threshold the latency is, rounded up: 17 ms is 0.125
|
|
106
|
+
// steps up and lands on the boundary one step up, 24 ms. Rounding up is what keeps the
|
|
107
|
+
// set of keys fixed when a latency is not a multiple of 8 ms.
|
|
108
|
+
//
|
|
109
|
+
// No EPSILON here, unlike the branch below: subtracting whole numbers gives a whole
|
|
110
|
+
// number, and dividing by a power of two shifts a binary float's exponent without
|
|
111
|
+
// touching its digits, so a latency on a boundary cannot come out just above a whole
|
|
112
|
+
// number of steps.
|
|
113
|
+
var steps = Math.ceil((latencyMs - REPORTING_THRESHOLD_MS) / EVENLY_SPACED_STEP_MS);
|
|
114
|
+
return REPORTING_THRESHOLD_MS + steps * EVENLY_SPACED_STEP_MS;
|
|
115
|
+
}
|
|
116
|
+
var boundary = firstBoundaryAtOrAbove(latencyMs);
|
|
117
|
+
|
|
118
|
+
// A latency in the lower part of a split bucket is reported as the threshold itself, so the
|
|
119
|
+
// threshold is counted exactly.
|
|
120
|
+
var _iterator = _createForOfIteratorHelper(EXACT_THRESHOLD_BUCKETS),
|
|
121
|
+
_step;
|
|
122
|
+
try {
|
|
123
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
124
|
+
var _step$value = _step.value,
|
|
125
|
+
thresholdMs = _step$value.thresholdMs,
|
|
126
|
+
bucketMs = _step$value.bucketMs;
|
|
127
|
+
if (boundary === bucketMs && latencyMs <= thresholdMs) {
|
|
128
|
+
return thresholdMs;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
_iterator.e(err);
|
|
133
|
+
} finally {
|
|
134
|
+
_iterator.f();
|
|
135
|
+
}
|
|
136
|
+
return boundary;
|
|
137
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.InteractionGroup = void 0;
|
|
8
|
+
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
|
|
9
|
+
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
|
|
10
|
+
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
11
|
+
var _bucketBoundaries = require("./bucket-boundaries");
|
|
12
|
+
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; } } }; }
|
|
13
|
+
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; } }
|
|
14
|
+
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; }
|
|
15
|
+
/**
|
|
16
|
+
* Latencies are counted per 8 ms, the resolution Event Timing reports durations at. Every
|
|
17
|
+
* latency is rounded up to this step on the way in, which caps the number of distinct values
|
|
18
|
+
* a group can hold whatever the latency was derived from.
|
|
19
|
+
*/
|
|
20
|
+
var RESOLUTION_MS = 8;
|
|
21
|
+
|
|
22
|
+
/** Which percentiles are reported, as quantiles. */
|
|
23
|
+
var REPORTED_QUANTILES = [0.9, 0.98];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The latencies of one set of interactions — every interaction on the page, or only the ones
|
|
27
|
+
* inside the editor — reported as one object in the event.
|
|
28
|
+
*
|
|
29
|
+
* The whole state is a count per distinct latency, so an interaction only costs a counter
|
|
30
|
+
* whatever its latency was, and everything the event carries — the reported buckets, the
|
|
31
|
+
* count, the sum, the maximum and the percentiles — is derived from that map when a snapshot
|
|
32
|
+
* is taken. Nothing is computed while interactions arrive.
|
|
33
|
+
*/
|
|
34
|
+
var InteractionGroup = exports.InteractionGroup = /*#__PURE__*/function () {
|
|
35
|
+
function InteractionGroup() {
|
|
36
|
+
(0, _classCallCheck2.default)(this, InteractionGroup);
|
|
37
|
+
(0, _defineProperty2.default)(this, "countByLatency", new Map());
|
|
38
|
+
}
|
|
39
|
+
return (0, _createClass2.default)(InteractionGroup, [{
|
|
40
|
+
key: "add",
|
|
41
|
+
value: function add(latencyMs) {
|
|
42
|
+
this.increment(latencyMs);
|
|
43
|
+
}
|
|
44
|
+
}, {
|
|
45
|
+
key: "remeasure",
|
|
46
|
+
value: function remeasure(previousLatencyMs, latencyMs) {
|
|
47
|
+
// Moved rather than counted again: the count belongs to the same interaction.
|
|
48
|
+
this.decrement(previousLatencyMs);
|
|
49
|
+
this.increment(latencyMs);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param totalCount every interaction of the group, including those below the Event Timing
|
|
54
|
+
* reporting threshold, which this group never sees.
|
|
55
|
+
*/
|
|
56
|
+
}, {
|
|
57
|
+
key: "snapshot",
|
|
58
|
+
value: function snapshot(totalCount) {
|
|
59
|
+
var _latencies;
|
|
60
|
+
// Ascending, so the reported buckets come out in order and the last latency is the
|
|
61
|
+
// maximum. Sorted once for everything below.
|
|
62
|
+
var latencies = Array.from(this.countByLatency.keys()).sort(function (a, b) {
|
|
63
|
+
return a - b;
|
|
64
|
+
});
|
|
65
|
+
var observedCount = this.observedCount();
|
|
66
|
+
var percentileRanks = REPORTED_QUANTILES.map(function (quantile) {
|
|
67
|
+
return {
|
|
68
|
+
key: String(Math.round(quantile * 100)),
|
|
69
|
+
rank: Math.max(1, Math.ceil(quantile * observedCount))
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
var buckets = {};
|
|
73
|
+
var percentilesMs = {};
|
|
74
|
+
var sumMs = 0;
|
|
75
|
+
var counted = 0;
|
|
76
|
+
var _iterator = _createForOfIteratorHelper(latencies),
|
|
77
|
+
_step;
|
|
78
|
+
try {
|
|
79
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
80
|
+
var _this$countByLatency$, _buckets$bucket;
|
|
81
|
+
var latencyMs = _step.value;
|
|
82
|
+
var count = (_this$countByLatency$ = this.countByLatency.get(latencyMs)) !== null && _this$countByLatency$ !== void 0 ? _this$countByLatency$ : 0;
|
|
83
|
+
sumMs += latencyMs * count;
|
|
84
|
+
var bucket = String((0, _bucketBoundaries.bucketKeyForMs)(latencyMs));
|
|
85
|
+
buckets[bucket] = ((_buckets$bucket = buckets[bucket]) !== null && _buckets$bucket !== void 0 ? _buckets$bucket : 0) + count;
|
|
86
|
+
|
|
87
|
+
// A percentile is the latency the group's interactions reach counting up from the
|
|
88
|
+
// fastest, so it is answered as soon as this many of them have been passed.
|
|
89
|
+
counted += count;
|
|
90
|
+
var _iterator2 = _createForOfIteratorHelper(percentileRanks),
|
|
91
|
+
_step2;
|
|
92
|
+
try {
|
|
93
|
+
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
|
|
94
|
+
var _step2$value = _step2.value,
|
|
95
|
+
key = _step2$value.key,
|
|
96
|
+
rank = _step2$value.rank;
|
|
97
|
+
if (percentilesMs[key] === undefined && counted >= rank) {
|
|
98
|
+
percentilesMs[key] = latencyMs;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
_iterator2.e(err);
|
|
103
|
+
} finally {
|
|
104
|
+
_iterator2.f();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
_iterator.e(err);
|
|
109
|
+
} finally {
|
|
110
|
+
_iterator.f();
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
// `performance.interactionCount` can lag the entries the observer has delivered.
|
|
114
|
+
totalCount: Math.max(totalCount, observedCount),
|
|
115
|
+
observedCount: observedCount,
|
|
116
|
+
sumMs: sumMs,
|
|
117
|
+
maxMs: (_latencies = latencies[latencies.length - 1]) !== null && _latencies !== void 0 ? _latencies : 0,
|
|
118
|
+
buckets: buckets,
|
|
119
|
+
percentilesMs: percentilesMs
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}, {
|
|
123
|
+
key: "observedCount",
|
|
124
|
+
value: function observedCount() {
|
|
125
|
+
var total = 0;
|
|
126
|
+
var _iterator3 = _createForOfIteratorHelper(this.countByLatency.values()),
|
|
127
|
+
_step3;
|
|
128
|
+
try {
|
|
129
|
+
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
|
|
130
|
+
var count = _step3.value;
|
|
131
|
+
total += count;
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
_iterator3.e(err);
|
|
135
|
+
} finally {
|
|
136
|
+
_iterator3.f();
|
|
137
|
+
}
|
|
138
|
+
return total;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Rounding lives here so that every count goes through the same step, in or out. */
|
|
142
|
+
}, {
|
|
143
|
+
key: "roundLatencyUp",
|
|
144
|
+
value: function roundLatencyUp(latencyMs) {
|
|
145
|
+
return Math.ceil(latencyMs / RESOLUTION_MS) * RESOLUTION_MS;
|
|
146
|
+
}
|
|
147
|
+
}, {
|
|
148
|
+
key: "increment",
|
|
149
|
+
value: function increment(latencyMs) {
|
|
150
|
+
var _this$countByLatency$2;
|
|
151
|
+
var step = this.roundLatencyUp(latencyMs);
|
|
152
|
+
this.countByLatency.set(step, ((_this$countByLatency$2 = this.countByLatency.get(step)) !== null && _this$countByLatency$2 !== void 0 ? _this$countByLatency$2 : 0) + 1);
|
|
153
|
+
}
|
|
154
|
+
}, {
|
|
155
|
+
key: "decrement",
|
|
156
|
+
value: function decrement(latencyMs) {
|
|
157
|
+
var _this$countByLatency$3;
|
|
158
|
+
var step = this.roundLatencyUp(latencyMs);
|
|
159
|
+
var next = ((_this$countByLatency$3 = this.countByLatency.get(step)) !== null && _this$countByLatency$3 !== void 0 ? _this$countByLatency$3 : 0) - 1;
|
|
160
|
+
if (next > 0) {
|
|
161
|
+
this.countByLatency.set(step, next);
|
|
162
|
+
} else {
|
|
163
|
+
this.countByLatency.delete(step);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}]);
|
|
167
|
+
}();
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.InteractionObserver = void 0;
|
|
8
|
+
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
|
|
9
|
+
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
|
|
10
|
+
var _bucketBoundaries = require("./bucket-boundaries");
|
|
11
|
+
/** `performance.interactionCount` is Chromium-only and absent from the DOM typings. */
|
|
12
|
+
var interactionCount = function interactionCount() {
|
|
13
|
+
return performance.interactionCount;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** `durationThreshold` is absent from the DOM typings for `PerformanceObserverInit`. */
|
|
17
|
+
/**
|
|
18
|
+
* Reports the interactions the browser observes to `onEntries`.
|
|
19
|
+
*
|
|
20
|
+
* `drain` and `stop` are safe to call before `start` and after each other, so a session
|
|
21
|
+
* that never started collecting needs no special handling.
|
|
22
|
+
*/
|
|
23
|
+
var InteractionObserver = exports.InteractionObserver = /*#__PURE__*/function () {
|
|
24
|
+
function InteractionObserver(onEntries) {
|
|
25
|
+
(0, _classCallCheck2.default)(this, InteractionObserver);
|
|
26
|
+
this.onEntries = onEntries;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Starts reporting interactions from this point on. Does nothing when already started, so
|
|
31
|
+
* a second call cannot leave an observer running with nobody to disconnect it.
|
|
32
|
+
*
|
|
33
|
+
* `buffered` is `false`: the entries the browser collected earlier are interactions with
|
|
34
|
+
* the page while the editor was still loading, and they belong to no session of ours.
|
|
35
|
+
*/
|
|
36
|
+
return (0, _createClass2.default)(InteractionObserver, [{
|
|
37
|
+
key: "start",
|
|
38
|
+
value: function start() {
|
|
39
|
+
var _this = this;
|
|
40
|
+
if (this.observer) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.observer = new PerformanceObserver(function (list) {
|
|
44
|
+
// Delay by a microtask to work around a Safari bug where the callback is
|
|
45
|
+
// invoked synchronously rather than in a separate task.
|
|
46
|
+
// See: https://github.com/GoogleChrome/web-vitals/issues/277
|
|
47
|
+
Promise.resolve().then(function () {
|
|
48
|
+
_this.onEntries(list.getEntries());
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
var init = {
|
|
52
|
+
type: 'event',
|
|
53
|
+
buffered: false,
|
|
54
|
+
// 16 ms is also the smallest value the spec honours; lower values are clamped.
|
|
55
|
+
durationThreshold: _bucketBoundaries.REPORTING_THRESHOLD_MS
|
|
56
|
+
};
|
|
57
|
+
this.observer.observe(init);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Synchronously reports the entries the browser has produced but not yet dispatched to
|
|
62
|
+
* the callback. A snapshot taken because the page is going away has to include them,
|
|
63
|
+
* because there is no later chance to.
|
|
64
|
+
*/
|
|
65
|
+
}, {
|
|
66
|
+
key: "drain",
|
|
67
|
+
value: function drain() {
|
|
68
|
+
var _this$observer;
|
|
69
|
+
var entries = (_this$observer = this.observer) === null || _this$observer === void 0 ? void 0 : _this$observer.takeRecords();
|
|
70
|
+
if (entries) {
|
|
71
|
+
this.onEntries(entries);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}, {
|
|
75
|
+
key: "stop",
|
|
76
|
+
value: function stop() {
|
|
77
|
+
var _this$observer2;
|
|
78
|
+
(_this$observer2 = this.observer) === null || _this$observer2 === void 0 || _this$observer2.disconnect();
|
|
79
|
+
this.observer = undefined;
|
|
80
|
+
}
|
|
81
|
+
}], [{
|
|
82
|
+
key: "isSupported",
|
|
83
|
+
value:
|
|
84
|
+
/**
|
|
85
|
+
* Whether the browser reports both things a session needs: `interactionId`, which groups
|
|
86
|
+
* entries into interactions, and `performance.interactionCount`, which counts the ones
|
|
87
|
+
* below the reporting threshold. Both are Chromium-only, and without the count
|
|
88
|
+
* `totalCount` would be indistinguishable from `observedCount`.
|
|
89
|
+
*/
|
|
90
|
+
function isSupported() {
|
|
91
|
+
if (typeof window === 'undefined' || typeof PerformanceObserver === 'undefined') {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (!('PerformanceEventTiming' in window) || !('interactionId' in PerformanceEventTiming.prototype)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (!PerformanceObserver.supportedEntryTypes.includes('event')) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return typeof interactionCount() === 'number';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Total interactions on the page since page load, including those below the reporting
|
|
105
|
+
* threshold that no observer ever sees. A property of the page, not of an observer.
|
|
106
|
+
*/
|
|
107
|
+
}, {
|
|
108
|
+
key: "readPageInteractionCount",
|
|
109
|
+
value: function readPageInteractionCount() {
|
|
110
|
+
var _interactionCount;
|
|
111
|
+
return (_interactionCount = interactionCount()) !== null && _interactionCount !== void 0 ? _interactionCount : 0;
|
|
112
|
+
}
|
|
113
|
+
}]);
|
|
114
|
+
}();
|