@player-ui/metrics-plugin 0.0.1-next.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.
- package/dist/index.cjs.js +374 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.esm.js +365 -0
- package/package.json +19 -0
- package/src/index.ts +2 -0
- package/src/metrics.ts +586 -0
- package/src/symbols.ts +4 -0
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import type { Player, PlayerPlugin } from '@player-ui/player';
|
|
2
|
+
import { SyncHook, SyncBailHook } from 'tapable';
|
|
3
|
+
import type { BeaconPluginPlugin, BeaconArgs } from '@player-ui/beacon-plugin';
|
|
4
|
+
import { BeaconPlugin } from '@player-ui/beacon-plugin';
|
|
5
|
+
import {
|
|
6
|
+
MetricsCorePluginSymbol,
|
|
7
|
+
MetricsViewBeaconPluginContextSymbol,
|
|
8
|
+
} from './symbols';
|
|
9
|
+
|
|
10
|
+
// Try to use performance.now() but fall back to Date.now() if you can't
|
|
11
|
+
export const defaultGetTime =
|
|
12
|
+
typeof performance === 'undefined'
|
|
13
|
+
? () => Date.now()
|
|
14
|
+
: () => performance.now();
|
|
15
|
+
|
|
16
|
+
export type Timing = {
|
|
17
|
+
/** Time this duration started (ms) */
|
|
18
|
+
startTime: number;
|
|
19
|
+
} & (
|
|
20
|
+
| {
|
|
21
|
+
/** Flag set if this is currently in progress */
|
|
22
|
+
completed: false;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
/** The stopwatch has stopped */
|
|
26
|
+
completed: true;
|
|
27
|
+
|
|
28
|
+
/** The time in (ms) that the process ended */
|
|
29
|
+
endTime: number;
|
|
30
|
+
|
|
31
|
+
/** The elapsed time of this event (ms) */
|
|
32
|
+
duration: number;
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export type NodeMetrics = Timing & {
|
|
37
|
+
/** The type of the flow-state */
|
|
38
|
+
stateType: string;
|
|
39
|
+
|
|
40
|
+
/** The name of the flow-state */
|
|
41
|
+
stateName: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type NodeRenderMetrics = NodeMetrics & {
|
|
45
|
+
/** Timing representing the initial render */
|
|
46
|
+
render: Timing;
|
|
47
|
+
|
|
48
|
+
/** An array of timings representing updates to the view */
|
|
49
|
+
updates: Array<Timing>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export interface PlayerFlowMetrics {
|
|
53
|
+
/** All metrics about a running flow */
|
|
54
|
+
flow?: {
|
|
55
|
+
/** The id of the flow these metrics are for */
|
|
56
|
+
id: string;
|
|
57
|
+
|
|
58
|
+
/** request time */
|
|
59
|
+
requestTime?: number;
|
|
60
|
+
|
|
61
|
+
/** A timeline of events for each node-state */
|
|
62
|
+
timeline: Array<NodeMetrics | NodeRenderMetrics>;
|
|
63
|
+
|
|
64
|
+
/** A timing measuring until the first interactive render */
|
|
65
|
+
interactive: Timing;
|
|
66
|
+
} & Timing;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const callbacks = [
|
|
70
|
+
'onFlowBegin',
|
|
71
|
+
'onFlowEnd',
|
|
72
|
+
'onInteractive',
|
|
73
|
+
'onNodeStart',
|
|
74
|
+
'onNodeEnd',
|
|
75
|
+
'onRenderStart',
|
|
76
|
+
'onRenderEnd',
|
|
77
|
+
'onUpdateStart',
|
|
78
|
+
'onUpdateEnd',
|
|
79
|
+
'onUpdate',
|
|
80
|
+
] as const;
|
|
81
|
+
|
|
82
|
+
/** Context structure for 'viewed' beacons rendering metrics */
|
|
83
|
+
export interface MetricsViewBeaconPluginContext {
|
|
84
|
+
/** Represents the time taken before the view is first rendered */
|
|
85
|
+
renderTime?: number;
|
|
86
|
+
/** request time */
|
|
87
|
+
requestTime?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Simple [BeaconPluginPlugin] that adds renderTime to 'viewed' beacons data */
|
|
91
|
+
export class MetricsViewBeaconPlugin implements BeaconPluginPlugin {
|
|
92
|
+
static Symbol = MetricsViewBeaconPluginContextSymbol;
|
|
93
|
+
public readonly symbol = MetricsViewBeaconPlugin.Symbol;
|
|
94
|
+
|
|
95
|
+
private metricsPlugin: MetricsCorePlugin;
|
|
96
|
+
|
|
97
|
+
private resolvePendingRenderTime: ((renderTime: number) => void) | undefined;
|
|
98
|
+
|
|
99
|
+
constructor(metricsPlugin: MetricsCorePlugin) {
|
|
100
|
+
this.metricsPlugin = metricsPlugin;
|
|
101
|
+
this.metricsPlugin.hooks.onRenderEnd.tap(
|
|
102
|
+
'MetricsViewBeaconPlugin',
|
|
103
|
+
(timing) => {
|
|
104
|
+
if (timing.completed && this.resolvePendingRenderTime) {
|
|
105
|
+
this.resolvePendingRenderTime(timing.duration);
|
|
106
|
+
this.resolvePendingRenderTime = undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
apply(beaconPlugin: BeaconPlugin) {
|
|
113
|
+
beaconPlugin.hooks.buildBeacon.intercept({
|
|
114
|
+
context: true,
|
|
115
|
+
call: (context, beacon) => {
|
|
116
|
+
if (context && (beacon as BeaconArgs).action === 'viewed') {
|
|
117
|
+
context[this.symbol] = this.buildContext();
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async buildContext(): Promise<MetricsViewBeaconPluginContext> {
|
|
124
|
+
return {
|
|
125
|
+
renderTime: await this.getRenderTime(),
|
|
126
|
+
requestTime: this.getRequestTime(),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async getRenderTime(): Promise<number> {
|
|
131
|
+
const { flow } = this.metricsPlugin.getMetrics();
|
|
132
|
+
|
|
133
|
+
if (flow) {
|
|
134
|
+
const lastItem = flow.timeline[flow.timeline.length - 1];
|
|
135
|
+
|
|
136
|
+
if ('render' in lastItem && lastItem.render.completed) {
|
|
137
|
+
return lastItem.render.duration;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
this.resolvePendingRenderTime = resolve;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private getRequestTime(): number | undefined {
|
|
147
|
+
const { flow } = this.metricsPlugin.getMetrics();
|
|
148
|
+
|
|
149
|
+
return flow?.requestTime;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface MetricsWebPluginOptions {
|
|
154
|
+
/** Called when a flow starts */
|
|
155
|
+
onFlowBegin?: (update: PlayerFlowMetrics) => void;
|
|
156
|
+
|
|
157
|
+
/** Called when a flow ends */
|
|
158
|
+
onFlowEnd?: (update: PlayerFlowMetrics) => void;
|
|
159
|
+
|
|
160
|
+
/** Called when a flow becomes interactive for the first time */
|
|
161
|
+
onInteractive?: (timing: Timing, update: PlayerFlowMetrics) => void;
|
|
162
|
+
|
|
163
|
+
/** Called when a new node is started */
|
|
164
|
+
onNodeStart?: (
|
|
165
|
+
nodeMetrics: NodeMetrics | NodeRenderMetrics,
|
|
166
|
+
update: PlayerFlowMetrics
|
|
167
|
+
) => void;
|
|
168
|
+
|
|
169
|
+
/** Called when a node is ended */
|
|
170
|
+
onNodeEnd?: (
|
|
171
|
+
nodeMetrics: NodeMetrics | NodeRenderMetrics,
|
|
172
|
+
update: PlayerFlowMetrics
|
|
173
|
+
) => void;
|
|
174
|
+
|
|
175
|
+
/** Called when rendering for a node begins */
|
|
176
|
+
onRenderStart?: (
|
|
177
|
+
timing: Timing,
|
|
178
|
+
nodeMetrics: NodeRenderMetrics,
|
|
179
|
+
update: PlayerFlowMetrics
|
|
180
|
+
) => void;
|
|
181
|
+
|
|
182
|
+
/** Called when rendering for a node ends */
|
|
183
|
+
onRenderEnd?: (
|
|
184
|
+
timing: Timing,
|
|
185
|
+
nodeMetrics: NodeRenderMetrics,
|
|
186
|
+
update: PlayerFlowMetrics
|
|
187
|
+
) => void;
|
|
188
|
+
|
|
189
|
+
/** Called when an update for a node begins */
|
|
190
|
+
onUpdateStart?: (
|
|
191
|
+
timing: Timing,
|
|
192
|
+
nodeMetrics: NodeRenderMetrics,
|
|
193
|
+
update: PlayerFlowMetrics
|
|
194
|
+
) => void;
|
|
195
|
+
|
|
196
|
+
/** Called when an update for a node ends */
|
|
197
|
+
onUpdateEnd?: (
|
|
198
|
+
timing: Timing,
|
|
199
|
+
nodeMetrics: NodeRenderMetrics,
|
|
200
|
+
update: PlayerFlowMetrics
|
|
201
|
+
) => void;
|
|
202
|
+
|
|
203
|
+
/** Callback to subscribe to updates for any metric */
|
|
204
|
+
onUpdate?: (metrics: PlayerFlowMetrics) => void;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A flag to set if you want to track render times for nodes
|
|
208
|
+
* This requires that the UI calls `renderEnd()` when the view is painted.
|
|
209
|
+
*/
|
|
210
|
+
trackRenderTime?: boolean;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A flag to set if you want to track update times for nodes
|
|
214
|
+
* This requires that the UI calls `renderEnd()` when the view is painted.
|
|
215
|
+
*/
|
|
216
|
+
trackUpdateTime?: boolean;
|
|
217
|
+
|
|
218
|
+
/** A function to get the current time (in ms) */
|
|
219
|
+
getTime?: () => number;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A plugin that enables request time metrics
|
|
224
|
+
*/
|
|
225
|
+
export class RequestTimeWebPlugin {
|
|
226
|
+
getRequestTime: () => number | undefined;
|
|
227
|
+
name = 'RequestTimeWebPlugin';
|
|
228
|
+
|
|
229
|
+
constructor(getRequestTime: () => number | undefined) {
|
|
230
|
+
this.getRequestTime = getRequestTime;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
apply(metricsCorePlugin: MetricsCorePlugin) {
|
|
234
|
+
metricsCorePlugin.hooks.resolveRequestTime.tap(this.name, () => {
|
|
235
|
+
return this.getRequestTime();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* A plugin that enables gathering of render metrics
|
|
242
|
+
*/
|
|
243
|
+
export class MetricsCorePlugin implements PlayerPlugin {
|
|
244
|
+
name = 'metrics';
|
|
245
|
+
|
|
246
|
+
static Symbol = MetricsCorePluginSymbol;
|
|
247
|
+
public readonly symbol = MetricsCorePluginSymbol;
|
|
248
|
+
|
|
249
|
+
protected trackRender: boolean;
|
|
250
|
+
protected trackUpdate: boolean;
|
|
251
|
+
protected getTime: () => number;
|
|
252
|
+
|
|
253
|
+
public readonly hooks = {
|
|
254
|
+
resolveRequestTime: new SyncBailHook<number>(['requestTime']),
|
|
255
|
+
|
|
256
|
+
onFlowBegin: new SyncHook<PlayerFlowMetrics>(['update']),
|
|
257
|
+
onFlowEnd: new SyncHook<PlayerFlowMetrics>(['update']),
|
|
258
|
+
|
|
259
|
+
onInteractive: new SyncHook<Timing, PlayerFlowMetrics>([
|
|
260
|
+
'timing',
|
|
261
|
+
'update',
|
|
262
|
+
]),
|
|
263
|
+
|
|
264
|
+
onNodeStart: new SyncHook<NodeMetrics | NodeRenderMetrics>([
|
|
265
|
+
'nodeMetrics',
|
|
266
|
+
'update',
|
|
267
|
+
]),
|
|
268
|
+
onNodeEnd: new SyncHook<NodeMetrics | NodeRenderMetrics>([
|
|
269
|
+
'nodeMetrics',
|
|
270
|
+
'update',
|
|
271
|
+
]),
|
|
272
|
+
|
|
273
|
+
onRenderStart: new SyncHook<Timing, NodeRenderMetrics, PlayerFlowMetrics>([
|
|
274
|
+
'timing',
|
|
275
|
+
'nodeMetrics',
|
|
276
|
+
'update',
|
|
277
|
+
]),
|
|
278
|
+
onRenderEnd: new SyncHook<Timing, NodeRenderMetrics, PlayerFlowMetrics>([
|
|
279
|
+
'timing',
|
|
280
|
+
'nodeMetrics',
|
|
281
|
+
'update',
|
|
282
|
+
]),
|
|
283
|
+
|
|
284
|
+
onUpdateStart: new SyncHook<Timing, NodeRenderMetrics, PlayerFlowMetrics>([
|
|
285
|
+
'timing',
|
|
286
|
+
'nodeMetrics',
|
|
287
|
+
'update',
|
|
288
|
+
]),
|
|
289
|
+
onUpdateEnd: new SyncHook<Timing, NodeRenderMetrics, PlayerFlowMetrics>([
|
|
290
|
+
'timing',
|
|
291
|
+
'nodeMetrics',
|
|
292
|
+
'update',
|
|
293
|
+
]),
|
|
294
|
+
|
|
295
|
+
onUpdate: new SyncHook<PlayerFlowMetrics>(['update']),
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
private metrics: PlayerFlowMetrics = {};
|
|
299
|
+
|
|
300
|
+
constructor(options?: MetricsWebPluginOptions) {
|
|
301
|
+
this.trackRender = options?.trackRenderTime ?? false;
|
|
302
|
+
this.trackUpdate = options?.trackUpdateTime ?? false;
|
|
303
|
+
this.getTime = options?.getTime ?? defaultGetTime;
|
|
304
|
+
|
|
305
|
+
/** fn to call the update hook */
|
|
306
|
+
const callOnUpdate = () => {
|
|
307
|
+
this.hooks.onUpdate.call(this.metrics);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
this.hooks.onFlowBegin.tap(this.name, callOnUpdate);
|
|
311
|
+
this.hooks.onFlowEnd.tap(this.name, callOnUpdate);
|
|
312
|
+
this.hooks.onInteractive.tap(this.name, callOnUpdate);
|
|
313
|
+
this.hooks.onNodeStart.tap(this.name, callOnUpdate);
|
|
314
|
+
this.hooks.onNodeEnd.tap(this.name, callOnUpdate);
|
|
315
|
+
|
|
316
|
+
this.hooks.onRenderStart.tap(this.name, callOnUpdate);
|
|
317
|
+
this.hooks.onRenderEnd.tap(this.name, callOnUpdate);
|
|
318
|
+
|
|
319
|
+
this.hooks.onUpdateStart.tap(this.name, callOnUpdate);
|
|
320
|
+
this.hooks.onUpdateEnd.tap(this.name, callOnUpdate);
|
|
321
|
+
|
|
322
|
+
callbacks.forEach((hookName) => {
|
|
323
|
+
if (options?.[hookName] !== undefined) {
|
|
324
|
+
this.hooks[hookName].tap('options', options?.[hookName] as any);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Fetch the metrics of the current flow
|
|
331
|
+
*/
|
|
332
|
+
public getMetrics(): PlayerFlowMetrics {
|
|
333
|
+
return this.metrics;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Called when the UI layer wishes to start a timer for rendering */
|
|
337
|
+
private renderStart(): void {
|
|
338
|
+
// Grab the last update
|
|
339
|
+
const timeline = this.metrics.flow?.timeline;
|
|
340
|
+
|
|
341
|
+
if (!timeline || timeline.length === 0) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const lastItem = timeline[timeline.length - 1];
|
|
346
|
+
|
|
347
|
+
if ('updates' in lastItem) {
|
|
348
|
+
// Get the last update, make sure it's completed
|
|
349
|
+
if (lastItem.updates.length > 0) {
|
|
350
|
+
const lastUpdate = lastItem.updates[lastItem.updates.length - 1];
|
|
351
|
+
|
|
352
|
+
if (lastUpdate.completed === false) {
|
|
353
|
+
// Starting a new render before the last one was finished.
|
|
354
|
+
// Just ignore it and include as part of 1 render time
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (!lastItem.render.completed) {
|
|
360
|
+
// Starting a new render before the last one was finished.
|
|
361
|
+
// Just ignore it and include as part of 1 render time
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const update: Timing = {
|
|
366
|
+
completed: false,
|
|
367
|
+
startTime: defaultGetTime(),
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
lastItem.updates.push(update);
|
|
371
|
+
|
|
372
|
+
this.hooks.onUpdateStart.call(update, lastItem, this.metrics);
|
|
373
|
+
} else {
|
|
374
|
+
const renderInfo = {
|
|
375
|
+
...lastItem,
|
|
376
|
+
render: {
|
|
377
|
+
completed: false,
|
|
378
|
+
startTime: defaultGetTime(),
|
|
379
|
+
},
|
|
380
|
+
updates: [],
|
|
381
|
+
} as NodeRenderMetrics;
|
|
382
|
+
|
|
383
|
+
timeline[timeline.length - 1] = renderInfo;
|
|
384
|
+
|
|
385
|
+
this.hooks.onRenderStart.call(
|
|
386
|
+
renderInfo.render,
|
|
387
|
+
renderInfo,
|
|
388
|
+
this.metrics
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Called when the UI layer wants to end the rendering timer */
|
|
394
|
+
public renderEnd(): void {
|
|
395
|
+
if (!this.trackRender) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
'Must start the metrics-plugin with render tracking enabled'
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const { flow } = this.metrics;
|
|
402
|
+
|
|
403
|
+
if (!flow) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const { timeline, interactive } = flow;
|
|
408
|
+
|
|
409
|
+
if (!timeline || !interactive || timeline.length === 0) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const lastItem = timeline[timeline.length - 1];
|
|
414
|
+
|
|
415
|
+
if (!('render' in lastItem)) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Check if this is an update or render
|
|
420
|
+
const endTime = defaultGetTime();
|
|
421
|
+
|
|
422
|
+
if (lastItem.render.completed) {
|
|
423
|
+
// This is the end of an existing update
|
|
424
|
+
|
|
425
|
+
if (lastItem.updates.length === 0) {
|
|
426
|
+
// throw new Error("Trying to end an update that's not in progress");
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const lastUpdate = lastItem.updates[lastItem.updates.length - 1];
|
|
431
|
+
|
|
432
|
+
if (lastUpdate.completed === true) {
|
|
433
|
+
// throw new Error("Trying to end an update that's not in progress");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const update = {
|
|
438
|
+
...lastUpdate,
|
|
439
|
+
completed: true,
|
|
440
|
+
endTime,
|
|
441
|
+
duration: endTime - lastUpdate.startTime,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
lastItem.updates[lastItem.updates.length - 1] = update;
|
|
445
|
+
this.hooks.onUpdateEnd.call(update, lastItem, this.metrics);
|
|
446
|
+
} else {
|
|
447
|
+
lastItem.render = {
|
|
448
|
+
...lastItem.render,
|
|
449
|
+
completed: true,
|
|
450
|
+
endTime,
|
|
451
|
+
duration: endTime - lastItem.startTime,
|
|
452
|
+
};
|
|
453
|
+
this.hooks.onRenderEnd.call(lastItem.render, lastItem, this.metrics);
|
|
454
|
+
|
|
455
|
+
if (!interactive.completed) {
|
|
456
|
+
flow.interactive = {
|
|
457
|
+
...interactive,
|
|
458
|
+
completed: true,
|
|
459
|
+
duration: endTime - interactive.startTime,
|
|
460
|
+
endTime,
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
this.hooks.onInteractive.call(flow.interactive, this.metrics);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
apply(player: Player): void {
|
|
469
|
+
player.hooks.onStart.tap(this.name, (flow) => {
|
|
470
|
+
const requestTime = this.hooks.resolveRequestTime.call();
|
|
471
|
+
const startTime = defaultGetTime();
|
|
472
|
+
this.metrics = {
|
|
473
|
+
flow: {
|
|
474
|
+
id: flow.id,
|
|
475
|
+
requestTime,
|
|
476
|
+
timeline: [],
|
|
477
|
+
startTime,
|
|
478
|
+
completed: false,
|
|
479
|
+
interactive: {
|
|
480
|
+
completed: false,
|
|
481
|
+
startTime,
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
this.hooks.onFlowBegin.call(this.metrics);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
player.hooks.state.tap(this.name, (state) => {
|
|
490
|
+
if (state.status === 'completed' || state.status === 'error') {
|
|
491
|
+
const endTime = defaultGetTime();
|
|
492
|
+
const { flow } = this.metrics;
|
|
493
|
+
|
|
494
|
+
if (flow === undefined || flow?.completed === true) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
this.metrics = {
|
|
499
|
+
flow: {
|
|
500
|
+
...flow,
|
|
501
|
+
completed: true,
|
|
502
|
+
endTime,
|
|
503
|
+
duration: endTime - flow.startTime,
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// get the last update
|
|
508
|
+
|
|
509
|
+
const lastUpdate = flow.timeline[flow.timeline.length - 1];
|
|
510
|
+
|
|
511
|
+
if (lastUpdate && !lastUpdate.completed) {
|
|
512
|
+
(this.metrics.flow as any).timeline[flow.timeline.length - 1] = {
|
|
513
|
+
...lastUpdate,
|
|
514
|
+
completed: true,
|
|
515
|
+
endTime,
|
|
516
|
+
duration: endTime - lastUpdate.startTime,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
this.hooks.onFlowEnd.call(this.metrics);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
player.hooks.flowController.tap(this.name, (fc) => {
|
|
525
|
+
fc.hooks.flow.tap(this.name, (f) => {
|
|
526
|
+
f.hooks.transition.tap(this.name, (from, to) => {
|
|
527
|
+
const time = defaultGetTime();
|
|
528
|
+
const { flow } = this.metrics;
|
|
529
|
+
|
|
530
|
+
if (!flow) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const { timeline } = flow;
|
|
535
|
+
|
|
536
|
+
// End the last state, and start the next one
|
|
537
|
+
|
|
538
|
+
if (timeline.length > 0) {
|
|
539
|
+
const prev = timeline[timeline.length - 1];
|
|
540
|
+
|
|
541
|
+
if (prev.completed) {
|
|
542
|
+
throw new Error("Completing a state that's already done.");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
timeline[timeline.length - 1] = {
|
|
546
|
+
...prev,
|
|
547
|
+
completed: true,
|
|
548
|
+
endTime: time,
|
|
549
|
+
duration: time - prev.startTime,
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
this.hooks.onNodeEnd.call(timeline[timeline.length - 1]);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const nodeMetrics = {
|
|
556
|
+
completed: false,
|
|
557
|
+
startTime: time,
|
|
558
|
+
stateName: to.name,
|
|
559
|
+
stateType: to.value.state_type,
|
|
560
|
+
} as const;
|
|
561
|
+
|
|
562
|
+
timeline.push(nodeMetrics);
|
|
563
|
+
this.hooks.onNodeStart.call(nodeMetrics);
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
if (this.trackRender) {
|
|
569
|
+
player.hooks.view.tap(this.name, (v) => {
|
|
570
|
+
if (this.trackUpdate) {
|
|
571
|
+
v.hooks.onUpdate.tap(this.name, () => {
|
|
572
|
+
this.renderStart();
|
|
573
|
+
});
|
|
574
|
+
} else {
|
|
575
|
+
this.renderStart();
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
player.applyTo<BeaconPlugin>(BeaconPlugin.Symbol, (beaconPlugin) =>
|
|
580
|
+
new MetricsViewBeaconPlugin(this).apply(beaconPlugin)
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export default MetricsCorePlugin;
|
package/src/symbols.ts
ADDED