@atlaskit/insm 0.0.2 → 0.1.1

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 (38) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +203 -5
  3. package/dist/cjs/index.js +55 -1
  4. package/dist/cjs/inp-measurers/inp.js +184 -0
  5. package/dist/cjs/insm-period.js +345 -0
  6. package/dist/cjs/insm-session.js +183 -0
  7. package/dist/cjs/insm.js +129 -0
  8. package/dist/cjs/period-measurers/afps.js +193 -0
  9. package/dist/cjs/types.js +5 -0
  10. package/dist/es2019/index.js +54 -1
  11. package/dist/es2019/inp-measurers/inp.js +142 -0
  12. package/dist/es2019/insm-period.js +246 -0
  13. package/dist/es2019/insm-session.js +149 -0
  14. package/dist/es2019/insm.js +105 -0
  15. package/dist/es2019/period-measurers/afps.js +153 -0
  16. package/dist/es2019/types.js +1 -0
  17. package/dist/esm/index.js +54 -1
  18. package/dist/esm/inp-measurers/inp.js +177 -0
  19. package/dist/esm/insm-period.js +339 -0
  20. package/dist/esm/insm-session.js +177 -0
  21. package/dist/esm/insm.js +122 -0
  22. package/dist/esm/period-measurers/afps.js +186 -0
  23. package/dist/esm/types.js +1 -0
  24. package/dist/types/index.d.ts +10 -1
  25. package/dist/types/inp-measurers/inp.d.ts +37 -0
  26. package/dist/types/insm-period.d.ts +72 -0
  27. package/dist/types/insm-session.d.ts +91 -0
  28. package/dist/types/insm.d.ts +61 -0
  29. package/dist/types/period-measurers/afps.d.ts +57 -0
  30. package/dist/types/types.d.ts +81 -0
  31. package/dist/types-ts4.5/index.d.ts +10 -1
  32. package/dist/types-ts4.5/inp-measurers/inp.d.ts +37 -0
  33. package/dist/types-ts4.5/insm-period.d.ts +72 -0
  34. package/dist/types-ts4.5/insm-session.d.ts +91 -0
  35. package/dist/types-ts4.5/insm.d.ts +61 -0
  36. package/dist/types-ts4.5/period-measurers/afps.d.ts +57 -0
  37. package/dist/types-ts4.5/types.d.ts +81 -0
  38. package/package.json +6 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @atlaskit/insm
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 0.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`87ddd90aaae7b`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/87ddd90aaae7b) -
14
+ Add INP period measurer
15
+
3
16
  ## 0.0.2
4
17
 
5
18
  ### Patch Changes
package/README.md CHANGED
@@ -1,8 +1,206 @@
1
- # Insm
1
+ # insm — Interactivity Session Measurement
2
2
 
3
- INSM tooling measures user-perceived interactivity of a page
3
+ A lightweight, session-based way to measure how interactive your experience feels to users. insm
4
+ summarizes interactivity quality for the time a user spends on a page or specific view, and emits a
5
+ single analytics event when the session ends.
4
6
 
5
- ## Usage
7
+ ## What insm measures
6
8
 
7
- Detailed docs and example usage can be found
8
- [here](https://atlaskit.atlassian.com/packages/editor/insm).
9
+ - Interactivity quality during periods of user activity within a session
10
+ - Both active time (when users are interacting) and overall session time
11
+ - Excludes explicitly-marked heavy/expected work so it doesn’t skew results
12
+ - A single, session-completion event with summary statistics and optional custom properties
13
+
14
+ ## Common insm measurement tasks
15
+
16
+ ### Instrumenting "heavy tasks"
17
+
18
+ Heavy tasks which are known to cause interactivity regressions can be explicitly opted out of the
19
+ interactivity measurement via explicitly marking the period of heavy work.
20
+
21
+ Use with care—prefer reducing the cost of heavy tasks over opting out of measurement. As a general
22
+ rule, only initialization and non-core actions should be marked heavy.
23
+
24
+ Examples of "heavy task" periods are
25
+
26
+ - page initialisation
27
+ - when first loading a page — significant work will often be triggered to collect, arrange and
28
+ present the page content to the user
29
+ - paste handling
30
+ - for the edit-page paste handler — paste handling is currently known to be computationally
31
+ expensive (and for large clipboard items, can lock up the UI for seconds). Given this is a non
32
+ core experience — excluding this from interactivity monitoring ensures we have signal on the
33
+ core experience.
34
+
35
+ ```ts
36
+ insm.session.startHeavyTask('paste-handler');
37
+ // ... perform paste work ...
38
+ insm.session.endHeavyTask('paste-handler');
39
+ ```
40
+
41
+ ### Adding additional information to the session
42
+
43
+ Additional session information can be added through the `addProperties` api.
44
+
45
+ ```ts
46
+ type ValidProperty = string | number | boolean;
47
+ insm.session.addProperties(
48
+ propsOrFactory: Record<string, ValidProperty> | (() => Record<string, ValidProperty>)
49
+ ): void
50
+ ```
51
+
52
+ This api takes either a static single-level key-value object, or callbacks which return the same and
53
+ will be evaluated on session end.
54
+
55
+ When ending a session, all properties received via this api are merged, in order, into the resulting
56
+ insm event’s properties; last write wins.
57
+
58
+ Callback values are evaluated at session end.
59
+
60
+ For example, for the following
61
+
62
+ ```ts
63
+ insm.session.addProperties({ one: 1, two: 2 });
64
+ insm.session.addProperties(() => ({ one: 'one' }));
65
+ insm.session.addProperties({ three: 3 });
66
+ ```
67
+
68
+ The resulting added properties will be
69
+
70
+ ```ts
71
+ { one: 'one', two: 2, three: 3 }
72
+ ```
73
+
74
+ ### Getting current insm session details
75
+
76
+ ```ts
77
+ insm.session.details;
78
+ ```
79
+
80
+ This can be used to gate changes based on an individual experience.
81
+
82
+ ie.
83
+
84
+ ```ts
85
+ if (insm.session.details.experienceKey === 'cc.edit-page' && gateCheck()) {
86
+ insm.session.addProperties(() => ({
87
+ /*potentially large or computationally large property bag */
88
+ }));
89
+ }
90
+ ```
91
+
92
+ ### Flagging when a feature is being used
93
+
94
+ ```ts
95
+ insm.session.startFeature(featureName: string): void
96
+ insm.session.endFeature(featureName: string): void
97
+ ```
98
+
99
+ ```ts
100
+ useEffect(() => {
101
+ insm.session.startFeature('comment notification');
102
+ return () => {
103
+ insm.session.endFeature('comment notification');
104
+ };
105
+ }, []);
106
+ ```
107
+
108
+ In the resulting insm event;
109
+
110
+ - Long Animation frames will have any active features identified on them
111
+ - The slowest active sessions will have any features used during the active session identified on
112
+ them
113
+
114
+ ## How to instrument insm measurement
115
+
116
+ ### Prerequisites
117
+
118
+ Your product must have the insm library installed — insm has the same prerequisites as UFO tooling
119
+ https://developer.atlassian.com/platform/ufo/react-ufo/react-ufo/getting-started/#prerequisites.
120
+
121
+ ### Installation
122
+
123
+ ```sh
124
+ yarn add @atlaskit/insm
125
+ ```
126
+
127
+ ### Initialisation
128
+
129
+ To begin instrumentation, initialise insm via invoking the init function and passing a config object
130
+ as defined by your instrumentation requirements.
131
+
132
+ ```ts
133
+ import { init } from '@atlaskit/insm';
134
+
135
+ export function initialiseINSM(config) {
136
+ /* logic to get/initialise your app's analytics client */
137
+ init(config);
138
+ }
139
+ ```
140
+
141
+ As early as possible in the application mount timeline (ideally, before the application is mounted),
142
+ initialise the insm client via the initialiseINSM function.
143
+
144
+ **Important**: insm will only track interactivity for explicitly allow-listed experiences — these
145
+ must be configured in the `config` passed.
146
+
147
+ ```ts
148
+ {
149
+ getAnalyticsWebClient,
150
+ experiences: {
151
+ ['experience key']: {enabled: true}
152
+ }
153
+ }
154
+ ```
155
+
156
+ If an experience key is missing or not enabled, no session event will be emitted.
157
+
158
+ ### Starting the page session
159
+
160
+ On route change/start call the insm start api.
161
+
162
+ ```ts
163
+ insm.start(experienceKey, {
164
+ contentId,
165
+ initial,
166
+ });
167
+ ```
168
+
169
+ Note: calling this will end any existing experience (unless the experience key and properties
170
+ match - in which case no session change will occur).
171
+
172
+ #### Naming guidance
173
+
174
+ Choose an `experienceKey` that reflects the product and content type you want to analyze.
175
+
176
+ Examples: `cc.view-page`, `cc.edit-page`, `cc.live-doc`
177
+
178
+ ### Stop interactivity tracking for the page session
179
+
180
+ Ending a page session interactivity tracking is done by either;
181
+
182
+ - starting a new session.
183
+ - or when a tab session ends (ie. closing tab, refreshing page)
184
+
185
+ In some scenarios (ie. when a page error boundary is hit), you will want to exit early. This is
186
+ achieved by calling the following.
187
+
188
+ ```ts
189
+ insm.stopEarly(reasonKey: string, description: string);
190
+ ```
191
+
192
+ Sessions closed early are identifiable by their end details
193
+ `"endDetails": { stoppedBy: "early-stop", reasonKey, description }`.
194
+
195
+ **Note**: The session is ended as soon as stopEarly is called, any `addProperties` handlers will
196
+ called at this point.
197
+
198
+ ### Tracking gate access
199
+
200
+ Call gateAccess whenever a gate is evaluated or changes during the session.
201
+
202
+ ```ts
203
+ insm.gateAccess('gate key', 'gate rule');
204
+ ```
205
+
206
+ This should be wired up to any gate access libraries/tools used by your product.
package/dist/cjs/index.js CHANGED
@@ -4,7 +4,61 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.init = init;
7
+ exports.insm = void 0;
8
+ var _insm = require("./insm");
9
+ var initialisedInsm;
10
+
7
11
  /**
8
12
  * Initializes the INSM (Interactivity Session Measurement) tooling
9
13
  */
10
- function init() {}
14
+ function init(options) {
15
+ initialisedInsm = new _insm.INSM(options);
16
+ }
17
+ function insmInitialised() {
18
+ if (!initialisedInsm) {
19
+ // eslint-disable-next-line no-console
20
+ console.error('INSM used when not initialised');
21
+ return false;
22
+ }
23
+ return true;
24
+ }
25
+
26
+ /**
27
+ * **In**teractivity **s**ession **m**onitoring
28
+ */
29
+ var insm = exports.insm = {
30
+ startHeavyTask: function startHeavyTask(heavyTaskName) {
31
+ if (insmInitialised()) {
32
+ initialisedInsm.startHeavyTask(heavyTaskName);
33
+ }
34
+ },
35
+ endHeavyTask: function endHeavyTask(heavyTaskName) {
36
+ if (insmInitialised()) {
37
+ initialisedInsm.endHeavyTask(heavyTaskName);
38
+ }
39
+ },
40
+ start: function start(experienceKey, experienceProperties) {
41
+ if (insmInitialised()) {
42
+ initialisedInsm.start(experienceKey, experienceProperties);
43
+ }
44
+ },
45
+ stopEarly: function stopEarly(reasonKey, description) {
46
+ if (insmInitialised()) {
47
+ initialisedInsm.stopEarly(reasonKey, description);
48
+ }
49
+ },
50
+ // We only expose details and feature start/stop to consumers
51
+ // as the other properties are internals for the insm and InsmPeriod
52
+ // to interact with the running session.
53
+ get session() {
54
+ if (insmInitialised()) {
55
+ return initialisedInsm.runningSession;
56
+ }
57
+ },
58
+ // @ts-expect-error Private method for testing purposes
59
+ __setAnalyticsWebClient: function __setAnalyticsWebClient(analyticsWebClient) {
60
+ if (initialisedInsm) {
61
+ initialisedInsm.analyticsWebClient = analyticsWebClient;
62
+ }
63
+ }
64
+ };
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.INPTracker = 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 INPTracker = exports.INPTracker = /*#__PURE__*/function () {
12
+ function INPTracker(options) {
13
+ (0, _classCallCheck2.default)(this, INPTracker);
14
+ /**
15
+ * INP stands for Interaction to Next Paint
16
+ */
17
+ (0, _defineProperty2.default)(this, "name", 'inp');
18
+ this.includedInteractions = (options === null || options === void 0 ? void 0 : options.includedInteractions) || ['pointerdown', 'pointerup', 'click', 'keydown', 'keyup'];
19
+ this.monitor = new InteractionTracker(this.includedInteractions);
20
+ }
21
+ return (0, _createClass2.default)(INPTracker, [{
22
+ key: "start",
23
+ value: function start(paused) {
24
+ var result = this.monitor.start(paused);
25
+ return result;
26
+ }
27
+ }, {
28
+ key: "end",
29
+ value: function end() {
30
+ var result = this.monitor.end();
31
+ return result;
32
+ }
33
+ }, {
34
+ key: "pause",
35
+ value: function pause() {
36
+ this.monitor.pause();
37
+ }
38
+ }, {
39
+ key: "resume",
40
+ value: function resume() {
41
+ this.monitor.resume();
42
+ }
43
+ }]);
44
+ }();
45
+ var InteractionResult = /*#__PURE__*/function () {
46
+ function InteractionResult() {
47
+ (0, _classCallCheck2.default)(this, InteractionResult);
48
+ this.min = Infinity;
49
+ this.max = 0;
50
+ this.average = 0;
51
+ this.numerator = 0;
52
+ this.count = 0;
53
+ }
54
+ return (0, _createClass2.default)(InteractionResult, [{
55
+ key: "update",
56
+ value: function update(duration) {
57
+ if (duration > this.max) {
58
+ this.max = duration;
59
+ }
60
+ if (duration < this.min) {
61
+ this.min = duration;
62
+ }
63
+ this.numerator += this.average * (this.count - 1) + duration;
64
+ this.count += 1;
65
+ this.average = this.numerator / this.count;
66
+ }
67
+ }, {
68
+ key: "toMeasure",
69
+ value: function toMeasure() {
70
+ return {
71
+ min: this.count === 0 ? 0 : this.min,
72
+ max: this.max,
73
+ average: this.average,
74
+ numerator: this.numerator,
75
+ denominator: this.count
76
+ };
77
+ }
78
+ }]);
79
+ }();
80
+ var InteractionTracker = /*#__PURE__*/function () {
81
+ function InteractionTracker(includedInteractions) {
82
+ (0, _classCallCheck2.default)(this, InteractionTracker);
83
+ (0, _defineProperty2.default)(this, "paused", false);
84
+ this.performanceObserver = null;
85
+ this.interactionResult = new InteractionResult();
86
+ this.includedInteractions = includedInteractions;
87
+ }
88
+ return (0, _createClass2.default)(InteractionTracker, [{
89
+ key: "stopTracking",
90
+ value: function stopTracking() {
91
+ var _this$performanceObse;
92
+ (_this$performanceObse = this.performanceObserver) === null || _this$performanceObse === void 0 || _this$performanceObse.disconnect();
93
+ this.performanceObserver = null;
94
+ }
95
+ }, {
96
+ key: "startTracking",
97
+ value: function startTracking() {
98
+ var _this = this;
99
+ this.performanceObserver = new PerformanceObserver(function (list) {
100
+ // Note: find link to actual safari issue .. good to get rid of this if Safari has fixed this
101
+ // Delay by a microtask to workaround a bug in Safari where the
102
+ // callback is invoked immediately, rather than in a separate task.
103
+ // See: https://github.com/GoogleChrome/web-vitals/issues/277
104
+ Promise.resolve().then(function () {
105
+ var entries = list.getEntries();
106
+ entries.forEach(function (entry) {
107
+ // Skip further processing for entries that cannot be INP candidates.
108
+ //
109
+ // When a user interacts with a web page, a user interaction (for example a click) usually triggers a sequence of events.
110
+ // To measure the latency of this series of events, the events share the same interactionId.
111
+ // An interactionId is only computed for the following event types belonging to a user interaction. It is 0 otherwise.
112
+ // * click / tap / drag events: 'pointerdown', 'pointerup', 'click'
113
+ // * keypress events: 'keydown', 'keyup'
114
+ //
115
+ if (!entry.interactionId) {
116
+ return;
117
+ }
118
+ if (_this.includedInteractions.includes(entry.name)) {
119
+ _this.interactionResult.update(entry.duration);
120
+ }
121
+ });
122
+ });
123
+ });
124
+
125
+ // Event Timing entries have their durations rounded to the nearest 8ms,
126
+ // so a duration of 40ms would be any event that spans 2.5 or more frames
127
+ // at 60Hz. This threshold is chosen to strike a balance between usefulness
128
+ // and performance. Running this callback for any interaction that spans
129
+ // just one or two frames is likely not worth the insight that could be
130
+ // gained.
131
+ if (PerformanceObserver.supportedEntryTypes.includes('event')) {
132
+ this.performanceObserver.observe({
133
+ type: 'event',
134
+ buffered: true,
135
+ durationThreshold: 40
136
+ });
137
+ }
138
+ }
139
+ }, {
140
+ key: "reset",
141
+ value: function reset() {
142
+ this.stopTracking();
143
+ this.interactionResult = new InteractionResult();
144
+ }
145
+ }, {
146
+ key: "start",
147
+ value: function start(paused) {
148
+ var lastResult = this.interactionResult.toMeasure();
149
+ this.reset();
150
+ this.paused = paused;
151
+ if (!paused) {
152
+ this.startTracking();
153
+ }
154
+ return lastResult;
155
+ }
156
+ }, {
157
+ key: "end",
158
+ value: function end() {
159
+ try {
160
+ return this.interactionResult.toMeasure();
161
+ } finally {
162
+ this.reset();
163
+ }
164
+ }
165
+ }, {
166
+ key: "pause",
167
+ value: function pause() {
168
+ if (this.paused) {
169
+ return;
170
+ }
171
+ this.paused = true;
172
+ this.stopTracking();
173
+ }
174
+ }, {
175
+ key: "resume",
176
+ value: function resume() {
177
+ if (!this.paused) {
178
+ return;
179
+ }
180
+ this.paused = false;
181
+ this.startTracking();
182
+ }
183
+ }]);
184
+ }();