openvibers/openvibe.live Commits
Real-user measurements: the ten most frequent script errors on /metrics too (live_rum_errors{message}), readable on the host without a staff session (#218)
Changes
@@@ -880,6 +880,7 @@ observability.registerDomainGauges(metricsRegistry, {outboxStatus: async () => await require('./events/stream-events').counts(),});metricsRegistry.gauge({ name: 'live_rum_p75', help: 'Real-user p75 per page kind and device class (ms; cls unitless), from the newest beacons', labelNames: ['metric', 'page', 'device'], collect: () => rum.p75Gauge() });++metricsRegistry.gauge({ name: 'live_rum_errors', help: 'Script errors visitors reported since this process started: the ten most frequent messages', labelNames: ['message'], collect: () => rum.errorsGauge() });app.get('/api/admin/rum', requireAuth, permissions.requireAdmin, (req, res) => { res.set('Cache-Control', 'no-store'); res.json(rum.snapshot()); });// ── Updates / Changelog ──────────────────────────────────────@@@ -11,6 +11,8 @@* POST /api/rum the beacon (text/plain JSON from navigator.sendBeacon); always 204* rum.snapshot() p50/p75/p95 per page kind × device, and the most frequent errors (GET /api/admin/rum)* rum.p75Gauge() the p75s for /metrics (live_rum_p75{metric,page,device})++ * rum.errorsGauge() the ten most frequent error messages for /metrics (live_rum_errors{message}), so they can++ * be read on the host without a staff session*/const express = require('express');@@@ -113,7 +115,11 @@ function createRum({ now = () => Date.now() } = {}) {return out;}-- return { router, record, snapshot, p75Gauge };++ function errorsGauge() {++ return [...errors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([message, count]) => ({ labels: { message: message.slice(0, 100) }, value: count }));++ }++++ return { router, record, snapshot, p75Gauge, errorsGauge };}module.exports = { createRum, pageKind, deviceClass };@@@ -56,6 +56,7 @@ const { createRum, pageKind, deviceClass } = require('../server/web/rum');const g = rum.p75Gauge();assert.ok(g.some((x) => x.labels.metric === 'lcp' && x.labels.page === 'home' && x.labels.device === 'phone' && x.value === 8000));assert.ok(!g.some((x) => x.labels.page === 'channel'), 'one channel sample is not a p75');++ assert.deepStrictEqual(rum.errorsGauge().map((x) => [x.labels.message, x.value]), [['TypeError: x is undefined', 1], ['b', 1], ['c', 1]]);// A flood from one address: 20 a minute, then dropped until the next minute.const before = rum.snapshot().pages['clip|phone'];
@@ -880,6 +880,7 @@ observability.registerDomainGauges(metricsRegistry, {
@@ -880,6 +880,7 @@ observability.registerDomainGauges(metricsRegistry, {
outboxStatus: async () => await require('./events/stream-events').counts(),
outboxStatus: async () => await require('./events/stream-events').counts(),
});
});
metricsRegistry.gauge({ name: 'live_rum_p75', help: 'Real-user p75 per page kind and device class (ms; cls unitless), from the newest beacons', labelNames: ['metric', 'page', 'device'], collect: () => rum.p75Gauge() });
metricsRegistry.gauge({ name: 'live_rum_p75', help: 'Real-user p75 per page kind and device class (ms; cls unitless), from the newest beacons', labelNames: ['metric', 'page', 'device'], collect: () => rum.p75Gauge() });
+metricsRegistry.gauge({ name: 'live_rum_errors', help: 'Script errors visitors reported since this process started: the ten most frequent messages', labelNames: ['message'], collect: () => rum.errorsGauge() });
app.get('/api/admin/rum', requireAuth, permissions.requireAdmin, (req, res) => { res.set('Cache-Control', 'no-store'); res.json(rum.snapshot()); });
app.get('/api/admin/rum', requireAuth, permissions.requireAdmin, (req, res) => { res.set('Cache-Control', 'no-store'); res.json(rum.snapshot()); });
// ── Updates / Changelog ──────────────────────────────────────
// ── Updates / Changelog ──────────────────────────────────────
@@ -11,6 +11,8 @@
@@ -11,6 +11,8 @@
* POST /api/rum the beacon (text/plain JSON from navigator.sendBeacon); always 204
* POST /api/rum the beacon (text/plain JSON from navigator.sendBeacon); always 204
* rum.snapshot() p50/p75/p95 per page kind × device, and the most frequent errors (GET /api/admin/rum)
* rum.snapshot() p50/p75/p95 per page kind × device, and the most frequent errors (GET /api/admin/rum)
* rum.p75Gauge() the p75s for /metrics (live_rum_p75{metric,page,device})
* rum.p75Gauge() the p75s for /metrics (live_rum_p75{metric,page,device})
+ * rum.errorsGauge() the ten most frequent error messages for /metrics (live_rum_errors{message}), so they can
+ * be read on the host without a staff session
*/
*/
const express = require('express');
const express = require('express');
@@ -113,7 +115,11 @@ function createRum({ now = () => Date.now() } = {}) {
@@ -113,7 +115,11 @@ function createRum({ now = () => Date.now() } = {}) {
return out;
return out;
}
}
- return { router, record, snapshot, p75Gauge };
+ function errorsGauge() {
+ return [...errors.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([message, count]) => ({ labels: { message: message.slice(0, 100) }, value: count }));
+ }
+
+ return { router, record, snapshot, p75Gauge, errorsGauge };
}
}
module.exports = { createRum, pageKind, deviceClass };
module.exports = { createRum, pageKind, deviceClass };
@@ -56,6 +56,7 @@ const { createRum, pageKind, deviceClass } = require('../server/web/rum');
@@ -56,6 +56,7 @@ const { createRum, pageKind, deviceClass } = require('../server/web/rum');
const g = rum.p75Gauge();
const g = rum.p75Gauge();
assert.ok(g.some((x) => x.labels.metric === 'lcp' && x.labels.page === 'home' && x.labels.device === 'phone' && x.value === 8000));
assert.ok(g.some((x) => x.labels.metric === 'lcp' && x.labels.page === 'home' && x.labels.device === 'phone' && x.value === 8000));
assert.ok(!g.some((x) => x.labels.page === 'channel'), 'one channel sample is not a p75');
assert.ok(!g.some((x) => x.labels.page === 'channel'), 'one channel sample is not a p75');
+ assert.deepStrictEqual(rum.errorsGauge().map((x) => [x.labels.message, x.value]), [['TypeError: x is undefined', 1], ['b', 1], ['c', 1]]);
// A flood from one address: 20 a minute, then dropped until the next minute.
// A flood from one address: 20 a minute, then dropped until the next minute.
const before = rum.snapshot().pages['clip|phone'];
const before = rum.snapshot().pages['clip|phone'];