@atlaskit/insm 0.0.2 → 0.1.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 +7 -0
- package/README.md +203 -5
- package/dist/cjs/index.js +55 -1
- package/dist/cjs/inp-measurers/inp.js +184 -0
- package/dist/cjs/insm-period.js +345 -0
- package/dist/cjs/insm-session.js +183 -0
- package/dist/cjs/insm.js +129 -0
- package/dist/cjs/period-measurers/afps.js +193 -0
- package/dist/cjs/types.js +5 -0
- package/dist/es2019/index.js +54 -1
- package/dist/es2019/inp-measurers/inp.js +142 -0
- package/dist/es2019/insm-period.js +246 -0
- package/dist/es2019/insm-session.js +149 -0
- package/dist/es2019/insm.js +105 -0
- package/dist/es2019/period-measurers/afps.js +153 -0
- package/dist/es2019/types.js +1 -0
- package/dist/esm/index.js +54 -1
- package/dist/esm/inp-measurers/inp.js +177 -0
- package/dist/esm/insm-period.js +339 -0
- package/dist/esm/insm-session.js +177 -0
- package/dist/esm/insm.js +122 -0
- package/dist/esm/period-measurers/afps.js +186 -0
- package/dist/esm/types.js +1 -0
- package/dist/types/index.d.ts +10 -1
- package/dist/types/inp-measurers/inp.d.ts +37 -0
- package/dist/types/insm-period.d.ts +72 -0
- package/dist/types/insm-session.d.ts +91 -0
- package/dist/types/insm.d.ts +61 -0
- package/dist/types/period-measurers/afps.d.ts +57 -0
- package/dist/types/types.d.ts +81 -0
- package/dist/types-ts4.5/index.d.ts +10 -1
- package/dist/types-ts4.5/inp-measurers/inp.d.ts +37 -0
- package/dist/types-ts4.5/insm-period.d.ts +72 -0
- package/dist/types-ts4.5/insm-session.d.ts +91 -0
- package/dist/types-ts4.5/insm.d.ts +61 -0
- package/dist/types-ts4.5/period-measurers/afps.d.ts +57 -0
- package/dist/types-ts4.5/types.d.ts +81 -0
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -1,8 +1,206 @@
|
|
|
1
|
-
#
|
|
1
|
+
# insm — Interactivity Session Measurement
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
7
|
+
## What insm measures
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
}();
|