@atlaskit/editor-plugin-interactivity 1.0.0 → 1.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +79 -0
  3. package/afm-cc/tsconfig.json +3 -0
  4. package/afm-products/tsconfig.json +3 -0
  5. package/dist/cjs/collections/bounded-list.js +42 -0
  6. package/dist/cjs/collections/bounded-map.js +42 -0
  7. package/dist/cjs/collector/interaction-events.js +0 -7
  8. package/dist/cjs/collector/interaction-group.js +31 -4
  9. package/dist/cjs/collector/interaction-tracker.js +210 -68
  10. package/dist/cjs/collector/interactivity-collector.js +52 -22
  11. package/dist/cjs/collector/interactivity-session.js +3 -0
  12. package/dist/cjs/collector/long-animation-frame-observer.js +62 -0
  13. package/dist/cjs/collector/slow-interaction-list.js +413 -0
  14. package/dist/es2019/collections/bounded-list.js +24 -0
  15. package/dist/es2019/collections/bounded-map.js +25 -0
  16. package/dist/es2019/collector/interaction-events.js +0 -7
  17. package/dist/es2019/collector/interaction-group.js +28 -5
  18. package/dist/es2019/collector/interaction-tracker.js +201 -55
  19. package/dist/es2019/collector/interactivity-collector.js +35 -20
  20. package/dist/es2019/collector/interactivity-session.js +3 -0
  21. package/dist/es2019/collector/long-animation-frame-observer.js +42 -0
  22. package/dist/es2019/collector/slow-interaction-list.js +342 -0
  23. package/dist/esm/collections/bounded-list.js +35 -0
  24. package/dist/esm/collections/bounded-map.js +35 -0
  25. package/dist/esm/collector/interaction-events.js +0 -7
  26. package/dist/esm/collector/interaction-group.js +31 -5
  27. package/dist/esm/collector/interaction-tracker.js +210 -68
  28. package/dist/esm/collector/interactivity-collector.js +52 -22
  29. package/dist/esm/collector/interactivity-session.js +3 -0
  30. package/dist/esm/collector/long-animation-frame-observer.js +55 -0
  31. package/dist/esm/collector/slow-interaction-list.js +407 -0
  32. package/dist/types/analytics/interactivity-snapshot.d.ts +65 -0
  33. package/dist/types/collections/bounded-list.d.ts +9 -0
  34. package/dist/types/collections/bounded-map.d.ts +9 -0
  35. package/dist/types/collector/interaction-events.d.ts +1 -7
  36. package/dist/types/collector/interaction-group.d.ts +18 -5
  37. package/dist/types/collector/interaction-tracker.d.ts +67 -13
  38. package/dist/types/collector/interactivity-collector.d.ts +3 -0
  39. package/dist/types/collector/interactivity-session.d.ts +2 -0
  40. package/dist/types/collector/long-animation-frame-observer.d.ts +28 -0
  41. package/dist/types/collector/slow-interaction-list.d.ts +64 -0
  42. package/docs/0-intro.tsx +2 -1
  43. package/package.json +4 -3
@@ -1,25 +1,46 @@
1
- import type { EditorInteractionGroupName } from './interaction-events';
1
+ import type { EditorInteractionGroupName } from '../analytics/interactivity-snapshot';
2
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.
3
+ * The Event Timing fields this package reads, declared optional so that a `PerformanceEntry` from
4
+ * `getEntries()` is assignable without a cast `interactionId` is missing from the DOM typings'
5
+ * `PerformanceEventTiming` altogether, and the rest are only on it.
6
6
  */
7
7
  export type InteractionEntry = PerformanceEntry & {
8
8
  interactionId?: number;
9
+ processingEnd?: number;
10
+ processingStart?: number;
11
+ target?: Node | null;
12
+ };
13
+ /**
14
+ * The four moments an interaction's latency divides at, in order: the user acted, its handlers
15
+ * started running, they finished, the screen updated.
16
+ */
17
+ export type InteractionBoundaries = {
18
+ presentedAt: number;
19
+ processingEndedAt: number;
20
+ processingStartedAt: number;
21
+ startedAt: number;
9
22
  };
10
23
  /**
11
24
  * 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.
25
+ * interaction, or it changed an interaction already known. Both carry the editor group of the
26
+ * interaction, if it is one of the editor's, and the boundaries it now has.
27
+ *
28
+ * A `remeasured` where `previousLatencyMs` equals `latencyMs` is an interaction whose latency stayed
29
+ * as it was and whose boundaries moved: the entry ran in the same paint without being the slowest
30
+ * of them.
14
31
  */
15
32
  export type InteractionUpdate = {
33
+ boundaries: InteractionBoundaries | undefined;
16
34
  group: EditorInteractionGroupName | undefined;
35
+ interactionId: number;
17
36
  latencyMs: number;
18
37
  type: 'new';
19
38
  } | {
20
- fromMs: number;
39
+ boundaries: InteractionBoundaries | undefined;
21
40
  group: EditorInteractionGroupName | undefined;
22
- toMs: number;
41
+ interactionId: number;
42
+ latencyMs: number;
43
+ previousLatencyMs: number;
23
44
  type: 'remeasured';
24
45
  };
25
46
  /**
@@ -36,11 +57,15 @@ export type InteractionUpdate = {
36
57
  *
37
58
  * The editor's events answer what an entry cannot: which interactions were with the editor, and
38
59
  * how many there were, including the ones below the Event Timing reporting threshold.
60
+ *
61
+ * Every entry is also placed in the paint that presented it, which is what says how an
62
+ * interaction's latency divides into waiting, processing and presentation. See `Paint`.
39
63
  */
40
64
  export declare class InteractionTracker {
41
65
  private readonly startsAfterInteractionId;
42
66
  private readonly interactions;
43
67
  private readonly groupByEvent;
68
+ private readonly recentPaints;
44
69
  private highestInteractionId;
45
70
  /**
46
71
  * @param startsAfterInteractionId interactions up to and including this one belong to the
@@ -55,11 +80,40 @@ export declare class InteractionTracker {
55
80
  /**
56
81
  * Merges an entry into the interaction it belongs to.
57
82
  *
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.
83
+ * @returns what that changed about the interactions this tracker knows, the entry's own first.
84
+ * More than one of them when the paint the entry ran in presented several.
60
85
  */
61
- merge(entry: InteractionEntry): InteractionUpdate | undefined;
62
- /** @returns the group of an interaction to count, when this is the event its group counts on. */
86
+ merge(entry: InteractionEntry): InteractionUpdate[];
63
87
  recordEditorEvent(event: Event): EditorInteractionGroupName | undefined;
64
- private prune;
88
+ /**
89
+ * Every interaction whose boundaries are read from this paint, reported as measured again at the
90
+ * latency it already had.
91
+ *
92
+ * @param except the interaction the entry measured, which the caller reports itself. `0` or
93
+ * nothing when the entry measured none, and then no interaction is left out.
94
+ */
95
+ private remeasuredUpdatesIn;
96
+ private newUpdate;
97
+ private remeasuredUpdate;
98
+ /**
99
+ * The four moments of an interaction, read from the paint as it stands now.
100
+ *
101
+ * Limited the way `web-vitals` limits its INP attribution, so the four stay in order: the paint's
102
+ * handlers can have started before the event arrived, and can have finished after the paint the
103
+ * event's rounded-down `duration` points at.
104
+ *
105
+ * @returns nothing when the browser reported no processing timestamps for the interaction, which
106
+ * leaves it in no paint.
107
+ */
108
+ private boundariesOf;
109
+ /**
110
+ * The paint that presented this entry, grown to cover this entry's own processing.
111
+ *
112
+ * The moment being matched is always the one the first entry of the paint reported, so that a
113
+ * run of entries 8 ms apart cannot walk one paint across the next.
114
+ *
115
+ * @returns nothing when the browser reported no processing timestamps for the entry, which
116
+ * leaves nothing to place it by.
117
+ */
118
+ private paintOf;
65
119
  }
@@ -26,6 +26,7 @@ export declare class InteractivityCollector {
26
26
  private readonly getObjectId;
27
27
  private readonly getSessionMode;
28
28
  private readonly interactionObserver;
29
+ private readonly longAnimationFrameObserver;
29
30
  private readonly editorEvents;
30
31
  private readonly snapshotScheduler;
31
32
  private readonly lifecycleObserver;
@@ -85,6 +86,8 @@ export declare class InteractivityCollector {
85
86
  * they were faster than its threshold — which is why a count alone moves the session on.
86
87
  */
87
88
  private recordEditorEvent;
89
+ /** Long Animation Frames say where the latency of a slow interaction went. */
90
+ private recordFrames;
88
91
  private recordEntries;
89
92
  private takeSnapshot;
90
93
  }
@@ -1,6 +1,7 @@
1
1
  import type { SessionMode } from '../analytics/interactivity-snapshot';
2
2
  import { InteractionTracker } from './interaction-tracker';
3
3
  import { InteractionGroup } from './interaction-group';
4
+ import { SlowInteractionList } from './slow-interaction-list';
4
5
  export type InteractivitySessionStart = {
5
6
  hidden: boolean;
6
7
  mode: SessionMode | undefined;
@@ -23,6 +24,7 @@ export declare class InteractivitySession {
23
24
  readonly editorTyping: InteractionGroup;
24
25
  readonly editorPointer: InteractionGroup;
25
26
  readonly editorOther: InteractionGroup;
27
+ readonly slowest: SlowInteractionList | undefined;
26
28
  /** Increments per snapshot; a query takes the highest one per session. */
27
29
  seq: number;
28
30
  /** Both fixed for the session: a change to either closes it and opens the next. */
@@ -0,0 +1,28 @@
1
+ export type LongAnimationFrame = PerformanceEntry & {
2
+ scripts?: LongAnimationFrameScript[];
3
+ styleAndLayoutStart?: number;
4
+ };
5
+ export type LongAnimationFrameScript = {
6
+ duration: number;
7
+ forcedStyleAndLayoutDuration?: number;
8
+ invokerType?: string;
9
+ sourceFunctionName?: string;
10
+ sourceURL?: string;
11
+ startTime: number;
12
+ };
13
+ /**
14
+ * Reports the frames the browser took longer than 50 ms to render to `onFrames`. They are what says
15
+ * where the time of a slow interaction went — which script ran the longest while the user waited,
16
+ * and how much of the wait was style and layout — which Event Timing cannot answer.
17
+ *
18
+ * What they say about an interaction is `SlowInteractionList`'s to work out; this only observes.
19
+ */
20
+ export declare class LongAnimationFrameObserver {
21
+ private readonly onFrames;
22
+ static isSupported(): boolean;
23
+ private observer;
24
+ constructor(onFrames: (frames: LongAnimationFrame[]) => void);
25
+ start(): void;
26
+ drain(): void;
27
+ stop(): void;
28
+ }
@@ -0,0 +1,64 @@
1
+ import type { SlowInteraction } from '../analytics/interactivity-snapshot';
2
+ import type { InteractionEntry, InteractionUpdate } from './interaction-tracker';
3
+ import type { LongAnimationFrame } from './long-animation-frame-observer';
4
+ /**
5
+ * The slowest interactions of one session, which is what the event's `slowest` records are.
6
+ *
7
+ * Interactions arrive from the tracker and frames from the Long Animation Frame observer, and this
8
+ * is where the two meet: a record says both how long the user waited and where that time went.
9
+ *
10
+ * A record is built from the entry that measured the interaction, as that entry arrives:
11
+ * `entry.target` is `null` once the element has left the document.
12
+ */
13
+ export declare class SlowInteractionList {
14
+ /** Slowest first. */
15
+ private readonly records;
16
+ private readonly frames;
17
+ /**
18
+ * Takes in what the tracker now says about an interaction, keeping it when it is one of the
19
+ * slowest of the session.
20
+ *
21
+ * @returns whether that changed what a snapshot would carry.
22
+ */
23
+ trackInteractionUpdate(entry: InteractionEntry, update: InteractionUpdate): boolean;
24
+ /**
25
+ * Takes in the frames the browser has just reported and works out again what the frames say about
26
+ * every record — again, because the frames of one interaction can be reported in several batches
27
+ * and the first of them may hold neither its longest script nor all of its style and layout.
28
+ *
29
+ * @returns whether that changed what a snapshot would carry.
30
+ */
31
+ trackLongAnimationFrames(frames: LongAnimationFrame[]): boolean;
32
+ snapshot(): SlowInteraction[] | undefined;
33
+ private toRecord;
34
+ private attribute;
35
+ /**
36
+ * What the frames say about an interaction, attributed the way `web-vitals` attributes INP: every
37
+ * frame overlapping the interaction counts, the script that counts is the one with the longest
38
+ * part inside it, and style and layout is summed across those frames.
39
+ *
40
+ * @returns nothing when no frame overlaps the interaction — the browser reports frames above
41
+ * 50 ms only.
42
+ */
43
+ private attributionFor;
44
+ /**
45
+ * Style, layout and paint of the frame, which the browser reports as starting at 0 when the
46
+ * frame did none.
47
+ */
48
+ private styleAndLayoutOf;
49
+ /** Which phase of the interaction the script ran in, by where it started. */
50
+ private subpartOf;
51
+ private truncated;
52
+ /**
53
+ * The file as the browser named it, content hash and all: that is what identifies the artefact
54
+ * and its source map, and a query can be grouped away downstream.
55
+ */
56
+ private fileName;
57
+ /**
58
+ * Names the element an interaction happened on — `div[data-vc="x"] > p > span`, outermost first.
59
+ * The path climbs until an element carries an allow-listed attribute, because that is what says
60
+ * which part of the page this was.
61
+ */
62
+ private describeTarget;
63
+ private identifyingAttribute;
64
+ }
package/docs/0-intro.tsx CHANGED
@@ -25,7 +25,8 @@ ${createEditorUseOnlyNotice('Editor Plugin Interactivity', [
25
25
 
26
26
  It reports the \`editor interactivity\` operational event: session-to-date interaction latency
27
27
  histograms for full page editor sessions — for the page as a whole, and for the editor's typing,
28
- pointer and other interactions — per [RFC 095](https://hello.atlassian.net/wiki/spaces/EDITOR/pages/7527607488/Editor+RFC+095+Confluence+editor+responsiveness+bucketed+INP+telemetry).
28
+ pointer and other interactions — along with the slowest interactions of the session, per
29
+ [RFC 095](https://hello.atlassian.net/wiki/spaces/EDITOR/pages/7527607488/Editor+RFC+095+Confluence+editor+responsiveness+bucketed+INP+telemetry).
29
30
  See the package README for the event shape, the snapshot cadence and what ends a session.
30
31
 
31
32
  The plugin has no configuration, state, actions or commands — adding it to a preset is what turns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-interactivity",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Interactivity plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -20,14 +20,15 @@
20
20
  "sideEffects": false,
21
21
  "dependencies": {
22
22
  "@atlaskit/browser-apis": "^1.2.0",
23
- "@atlaskit/editor-plugin-analytics": "^16.0.0",
23
+ "@atlaskit/editor-plugin-analytics": "^16.1.0",
24
24
  "@atlaskit/editor-plugin-context-identifier": "^16.0.0",
25
25
  "@atlaskit/editor-plugin-editor-viewmode": "^18.0.0",
26
+ "@atlaskit/platform-feature-experiments": "^0.3.0",
26
27
  "@babel/runtime": "^7.0.0",
27
28
  "bind-event-listener": "^3.0.0"
28
29
  },
29
30
  "peerDependencies": {
30
- "@atlaskit/editor-common": "^120.0.0",
31
+ "@atlaskit/editor-common": "^120.10.0",
31
32
  "react": "^18.2.0 || ^19.2.0"
32
33
  },
33
34
  "devDependencies": {