openvibers/openvibe.live Commits
Real-user measurements: time to a live channel's first picture (ttff, set by the player when live media first renders on a directly opened channel); beacons name the page they were loaded on, not where the SPA went since (#220)
Changes
@@@ -1,7 +1,7 @@--/* Real-user measurements (server/web/rum.js): this page load's own timings and up to three script errors, sent once-- * when the page is hidden. Loaded by app.js on one page load in four, after the page is idle; nothing identifies-- * the visitor (no account, no id, no cookie). The observers read buffered entries, so the paints before this-- * script loaded still count.++/* Real-user measurements (server/web/rum.js): this page load's own timings, the time to a live channel's first++ * picture (stream-player.js sets window.__ovFirstFrameAt) and up to three script errors, sent once when the page is++ * hidden. Loaded by app.js on one page load in four, after the page is idle; nothing identifies the visitor (no++ * account, no id, no cookie). The observers read buffered entries, so the paints before this script loaded count.*/(function () {'use strict';@@@ -31,8 +31,10 @@sent = true;const nav = performance.getEntriesByType && performance.getEntriesByType('navigation')[0];const r = (v) => Math.round(v);++ let path = location.pathname; // the page these timings are for: the one loaded, not where the SPA went since++ try { if (nav) path = new URL(nav.name).pathname; } catch (e) { /* keep */ }navigator.sendBeacon('/api/rum', JSON.stringify({-- path: location.pathname, w: innerWidth, ttfb: nav ? r(nav.responseStart) : 0,++ path, w: innerWidth, ttfb: nav ? r(nav.responseStart) : 0, ttff: r(window.__ovFirstFrameAt || 0),fcp: r(m.fcp), lcp: r(m.lcp), cls: Math.round(m.cls * 1000) / 1000, inp: r(m.inp), tbt: r(m.tbt), errs: m.errs,}));};@@@ -593,6 +593,10 @@ function _hidePlayerPlaceholder(surface = (playerType === 'jsmpeg' ? 'canvas' :playerLoadState.isSlow = false;_pushPlayerDiagnostic('phase.live', 'Playback is rendering live media');}++ // Real-user measurements (js/ov-rum.js): when the first live picture showed, for a channel opened directly.++ if (window.__ovFirstFrameAt == null) {++ try { const nav = performance.getEntriesByType('navigation')[0]; if (nav && new URL(nav.name).pathname === location.pathname) window.__ovFirstFrameAt = Math.round(performance.now()); } catch { /* */ }++ }}function _isVideoPlaybackLive(video) {@@@ -4,7 +4,8 @@* checks (scripts/perf, the browser smoke) run on one emulated phone; this is what people actually get.** public/js/ov-rum.js runs on one page load in four, after the page is idle, and sends one beacon when the page is-- * hidden: the path, the viewport width, and its own timings (TTFB, FCP, LCP, CLS, INP, blocking time) plus at most++ * hidden: the path, the viewport width, and its own timings (TTFB, FCP, LCP, CLS, INP, blocking time, and the time to a++ * live channel's first picture) plus at most* three error messages. Nothing about the visitor is kept: no IP, no account, no user agent. Each beacon becomes* numbers in a reservoir per page kind and device class, and errors are counted by message.*@@@ -16,7 +17,8 @@*/const express = require('express');--const METRICS = { ttfb: 60000, fcp: 60000, lcp: 60000, inp: 60000, tbt: 120000, cls: 10 };++// ttff: a live channel's first picture (stream-player.js), from navigation; 0 when no live picture showed.++const METRICS = { ttfb: 60000, fcp: 60000, lcp: 60000, inp: 60000, tbt: 120000, cls: 10, ttff: 120000 };const KEEP = 400; // samples per reservoir (the newest)const MAX_ERRORS = 100; // distinct error messages keptconst PER_MINUTE = 20; // beacons per address per minute (held in memory for that minute only)@@@ -51,6 +51,9 @@ const { createRum, pageKind, deviceClass } = require('../server/web/rum');assert.strictEqual(home.cls.p75, 0.012);assert.strictEqual(home.inp.n, 10, 'INP 0 is a real value (no slow interaction)');assert.strictEqual(snap.pages['channel|desktop'].lcp.p50, 2500);++ assert.strictEqual(snap.pages['channel|desktop'].ttff, undefined, 'no live picture: no ttff');++ await beacon({ path: '/@goosely', w: 1440, ttff: 1800 }, '10.0.1.4');++ assert.strictEqual(rum.snapshot().pages['channel|desktop'].ttff.p50, 1800, 'time to the first live picture');assert.deepStrictEqual(snap.errors.map((e) => e.message), ['TypeError: x is undefined', 'b', 'c'], 'at most three errors per beacon');// The gauge: p75 per metric, only for reservoirs with at least 5 samples.const g = rum.p75Gauge();
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
-/* Real-user measurements (server/web/rum.js): this page load's own timings and up to three script errors, sent once
- * when the page is hidden. Loaded by app.js on one page load in four, after the page is idle; nothing identifies
- * the visitor (no account, no id, no cookie). The observers read buffered entries, so the paints before this
- * script loaded still count.
+/* Real-user measurements (server/web/rum.js): this page load's own timings, the time to a live channel's first
+ * picture (stream-player.js sets window.__ovFirstFrameAt) and up to three script errors, sent once when the page is
+ * hidden. Loaded by app.js on one page load in four, after the page is idle; nothing identifies the visitor (no
+ * account, no id, no cookie). The observers read buffered entries, so the paints before this script loaded count.
*/
*/
(function () {
(function () {
'use strict';
'use strict';
@@ -31,8 +31,10 @@
@@ -31,8 +31,10 @@
sent = true;
sent = true;
const nav = performance.getEntriesByType && performance.getEntriesByType('navigation')[0];
const nav = performance.getEntriesByType && performance.getEntriesByType('navigation')[0];
const r = (v) => Math.round(v);
const r = (v) => Math.round(v);
+ let path = location.pathname; // the page these timings are for: the one loaded, not where the SPA went since
+ try { if (nav) path = new URL(nav.name).pathname; } catch (e) { /* keep */ }
navigator.sendBeacon('/api/rum', JSON.stringify({
navigator.sendBeacon('/api/rum', JSON.stringify({
- path: location.pathname, w: innerWidth, ttfb: nav ? r(nav.responseStart) : 0,
+ path, w: innerWidth, ttfb: nav ? r(nav.responseStart) : 0, ttff: r(window.__ovFirstFrameAt || 0),
fcp: r(m.fcp), lcp: r(m.lcp), cls: Math.round(m.cls * 1000) / 1000, inp: r(m.inp), tbt: r(m.tbt), errs: m.errs,
fcp: r(m.fcp), lcp: r(m.lcp), cls: Math.round(m.cls * 1000) / 1000, inp: r(m.inp), tbt: r(m.tbt), errs: m.errs,
}));
}));
};
};
@@ -593,6 +593,10 @@ function _hidePlayerPlaceholder(surface = (playerType === 'jsmpeg' ? 'canvas' :
@@ -593,6 +593,10 @@ function _hidePlayerPlaceholder(surface = (playerType === 'jsmpeg' ? 'canvas' :
playerLoadState.isSlow = false;
playerLoadState.isSlow = false;
_pushPlayerDiagnostic('phase.live', 'Playback is rendering live media');
_pushPlayerDiagnostic('phase.live', 'Playback is rendering live media');
}
}
+ // Real-user measurements (js/ov-rum.js): when the first live picture showed, for a channel opened directly.
+ if (window.__ovFirstFrameAt == null) {
+ try { const nav = performance.getEntriesByType('navigation')[0]; if (nav && new URL(nav.name).pathname === location.pathname) window.__ovFirstFrameAt = Math.round(performance.now()); } catch { /* */ }
+ }
}
}
function _isVideoPlaybackLive(video) {
function _isVideoPlaybackLive(video) {
@@ -4,7 +4,8 @@
@@ -4,7 +4,8 @@
* checks (scripts/perf, the browser smoke) run on one emulated phone; this is what people actually get.
* checks (scripts/perf, the browser smoke) run on one emulated phone; this is what people actually get.
*
*
* public/js/ov-rum.js runs on one page load in four, after the page is idle, and sends one beacon when the page is
* public/js/ov-rum.js runs on one page load in four, after the page is idle, and sends one beacon when the page is
- * hidden: the path, the viewport width, and its own timings (TTFB, FCP, LCP, CLS, INP, blocking time) plus at most
+ * hidden: the path, the viewport width, and its own timings (TTFB, FCP, LCP, CLS, INP, blocking time, and the time to a
+ * live channel's first picture) plus at most
* three error messages. Nothing about the visitor is kept: no IP, no account, no user agent. Each beacon becomes
* three error messages. Nothing about the visitor is kept: no IP, no account, no user agent. Each beacon becomes
* numbers in a reservoir per page kind and device class, and errors are counted by message.
* numbers in a reservoir per page kind and device class, and errors are counted by message.
*
*
@@ -16,7 +17,8 @@
@@ -16,7 +17,8 @@
*/
*/
const express = require('express');
const express = require('express');
-const METRICS = { ttfb: 60000, fcp: 60000, lcp: 60000, inp: 60000, tbt: 120000, cls: 10 };
+// ttff: a live channel's first picture (stream-player.js), from navigation; 0 when no live picture showed.
+const METRICS = { ttfb: 60000, fcp: 60000, lcp: 60000, inp: 60000, tbt: 120000, cls: 10, ttff: 120000 };
const KEEP = 400; // samples per reservoir (the newest)
const KEEP = 400; // samples per reservoir (the newest)
const MAX_ERRORS = 100; // distinct error messages kept
const MAX_ERRORS = 100; // distinct error messages kept
const PER_MINUTE = 20; // beacons per address per minute (held in memory for that minute only)
const PER_MINUTE = 20; // beacons per address per minute (held in memory for that minute only)
@@ -51,6 +51,9 @@ const { createRum, pageKind, deviceClass } = require('../server/web/rum');
@@ -51,6 +51,9 @@ const { createRum, pageKind, deviceClass } = require('../server/web/rum');
assert.strictEqual(home.cls.p75, 0.012);
assert.strictEqual(home.cls.p75, 0.012);
assert.strictEqual(home.inp.n, 10, 'INP 0 is a real value (no slow interaction)');
assert.strictEqual(home.inp.n, 10, 'INP 0 is a real value (no slow interaction)');
assert.strictEqual(snap.pages['channel|desktop'].lcp.p50, 2500);
assert.strictEqual(snap.pages['channel|desktop'].lcp.p50, 2500);
+ assert.strictEqual(snap.pages['channel|desktop'].ttff, undefined, 'no live picture: no ttff');
+ await beacon({ path: '/@goosely', w: 1440, ttff: 1800 }, '10.0.1.4');
+ assert.strictEqual(rum.snapshot().pages['channel|desktop'].ttff.p50, 1800, 'time to the first live picture');
assert.deepStrictEqual(snap.errors.map((e) => e.message), ['TypeError: x is undefined', 'b', 'c'], 'at most three errors per beacon');
assert.deepStrictEqual(snap.errors.map((e) => e.message), ['TypeError: x is undefined', 'b', 'c'], 'at most three errors per beacon');
// The gauge: p75 per metric, only for reservoirs with at least 5 samples.
// The gauge: p75 per metric, only for reservoirs with at least 5 samples.
const g = rum.p75Gauge();
const g = rum.p75Gauge();