openvibers/openvibe.media Commits
openvibe-shared v1.5.1 (Track R release manifest), openvibe-contracts v0.33.0, openvibe-sdk v0.5.0
Changes
@@@ -50,7 +50,7 @@ server/has_result, never params/result/error text/creator), worker (light/heavy lanes, leases, retries,cancellation), routes (/api/v2/:app/jobs), types: thumbnail.regenerate,invariant.scan (proposes split/remux), object.split, object.remux--(openvibe-shared: pinned OpenVibe.Shared v1.0.0 release, installed by npm)++(openvibe-shared v1.5.1, openvibe-contracts v0.33.0, openvibe-sdk v0.5.0: pinned release tarballs, installed by npm)scripts/smoke-test.sh end-to-end smoke test (boots a temp instance)scripts/backfill-objects.js / reconcile-objects.js / object-invariant.js / no-good-copy-report.js object-model operator toolsscripts/media-jobs.js list jobs, run the size-invariant scan (dry run by default), approve/cancel proposals@@@ -295,7 +295,9 @@ and checks each point.|---|---|| `GET /healthz` | liveness: the process answers (unchanged; it checks nothing else) || `GET /api/ready` | readiness from real checks, each with `status`, `required`, `latency_ms`, `checked_at`. **Required** (503 when one fails): `db` (a query against the SQLite database), `storage_vods`, `storage_clips`, `storage_pastes`, `storage_thumbnails`, `storage_files`, `storage_objects` (a probe file is written and removed). **Optional** (listed in `degraded`, still 200): `network_jwks` (user JWTs and service tokens; app keys work without it), `remote_b2` / `remote_r2` (HeadBucket, at most once a minute; present only when the provider is configured), `events_outbox` (backlog over 1000 events). Also `recordings_in_progress`. |--| `GET /metrics` | Prometheus text (openvibe-shared/metrics) for **direct loopback callers only**; 404 through nginx. HTTP golden signals by route template, process metrics, `release_info`, plus `media_recordings_in_progress`, `media_object_locations{provider,state}`, `media_objects{lifecycle_status}`, `media_events_outbox{status}` (only while the outbox runs). |++| `GET /metrics` | Prometheus text (openvibe-shared/metrics) for **direct loopback callers only**; 404 through nginx. HTTP golden signals by route template, process metrics, `release_info`, `release_client_updates_total{outcome,reason}`, plus `media_recordings_in_progress`, `media_object_locations{provider,state}`, `media_objects{lifecycle_status}`, `media_events_outbox{status}` (only while the outbox runs). |++| `GET /release.json` | release manifest (`registry.release-manifest@1` 1.1.0, openvibe-shared/release, ADR-016): deployed commit, package versions, components, contract ranges, `metrics_url`. The shared navbar's release-watch polls it. No components are declared, so every release still prompts open tabs to reload. |++| `POST /release-metrics` | open tabs' update reports (release-watch beacons, at most 4 KB; 30 a minute per connecting address, which behind nginx is the proxy, so one budget for all tabs; Sec-GPC/DNT dropped) into `release_client_updates_total` on `/metrics`. 403 in a restore drill, like every write. |## Public serving (no auth unless the item is private)@@@ -16,9 +16,9 @@"dotenv": "^16.4.5","express": "^4.19.2","multer": "^1.4.5-lts.1",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.28.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","sharp": "^0.33.4"}},@@@ -1919,9 +1919,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.28.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.28.0",-- "integrity": "sha512-cOOEmIhcjDmnWbo4jsz6kYOKT+Kc+aitYoc4w/EJOKg5uvJDt10vctjKB3IIl53UXeuDOhbckP53yBdBeewC1A==",++ "version": "0.33.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1935,17 +1935,18 @@}},"node_modules/openvibe-sdk": {-- "version": "0.2.2",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",-- "integrity": "sha512-kDifUp3EcmnTWzElAmaCZnX659WErcnm4+qVEoAiYQWX/w/1p4TWvoHbUPsrX65TSROVR3tiyVkl801n/0yyCw==",++ "version": "0.5.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",++ "integrity": "sha512-QVJkR5iazQX0ZwIrDp2A88ZeYYBSXEmSzhpgURPUSnzezIM3jz0HBhVKowyhkvg2aBO61XNf77eb4zJGkMjZiw==","license": "MIT","engines": {"node": ">=20"}},"node_modules/openvibe-shared": {-- "version": "1.3.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",++ "version": "1.5.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",++ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -11,8 +11,8 @@},"license": "MIT","dependencies": {-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.28.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0","@aws-sdk/client-s3": "^3.600.0","@aws-sdk/lib-storage": "^3.600.0","@aws-sdk/s3-request-presigner": "^3.600.0",@@@ -20,7 +20,7 @@"dotenv": "^16.4.5","express": "^4.19.2","multer": "^1.4.5-lts.1",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","sharp": "^0.33.4"}}@@@ -114,8 +114,10 @@ app.options(['/api/v1/:app/*', '/api/v2/:app/*'], (req, res) => {// ── Routes ───────────────────────────────────────────────────--// What this server runs (ADR-016); the shared navbar's release-watch polls it.--app.get('/release.json', release.handler);++// What this server runs (ADR-016); the shared navbar's release-watch polls it. POST /release-metrics++// takes open tabs' update reports into /metrics (release_client_updates_total). A restore drill++// answers that POST with the readOnly 403 above, like every other write.++release.mount(app, { registry: instrumented.registry });app.get('/healthz', (req, res) => {res.json({@@@ -14,7 +14,7 @@* - The endpoints the Host inventory compares (/release.json, the /browse index) and the watch pages* render from the copy; every route that would stream a stored file answers 503 without opening* the path the database gives it.-- * - Writes answer 403, sign-in 503, and the copy's rows stay as they were.++ * - Writes answer 403 (POST /release-metrics too), sign-in 503, and the copy's rows stay as they were.** Run: node test/drill-mode.test.js*/@@@ -271,6 +271,7 @@ const safe = { DB_PATH, HOST: '127.0.0.1', PORT: '14100' };['POST', '/api/v1/live/vods', { title: 'x' }], ['PUT', '/api/v1/live/vods/1', { title: 'y' }], ['DELETE', '/api/v1/live/vods/1', null],['POST', '/api/v1/live/clips', { vod_id: 1 }], ['PUT', `/api/v2/live/objects/${objectId}/content`, 'raw bytes'], ['POST', '/api/v2/live/jobs', { type: 'thumbnail' }],['POST', '/api/v1/live/admin/storage/tiers/sweep', {}], ['POST', '/internal/avatar-ingest', {}], ['PATCH', '/api/v1/live/vods/1', {}],++ ['POST', '/release-metrics', { counts: { reloaded: { user: 1 } } }],];for (const [m, p, body] of writes) {const r = await request(m, p, { headers: auth, body });@@@ -146,6 +146,24 @@ const listen = (app) => new Promise((resolve) => { const s = app.listen(0, '127.assert.ok(m.body.includes('http_requests_total{method="GET",route="/api/ready",status_class="2xx"} 1\n'), m.body.slice(0, 400));const h = await request(b2, '/healthz');assert.strictEqual(h.status, 200, '/healthz still answers');++ // The release manifest (registry.release-manifest@1) names where open tabs report updates,++ // and those reports land in /metrics.++ const rel = await request(b2, '/release.json');++ assert.strictEqual(rel.status, 200);++ const manifest = JSON.parse(rel.body);++ assert.strictEqual(manifest.service, 'media');++ assert.strictEqual(manifest.release, 'abcdef123456');++ assert.deepStrictEqual(require('openvibe-contracts').validate('registry.release-manifest@1', manifest).errors, []);++ assert.strictEqual(manifest.metrics_url, '/release-metrics');++ const report = await new Promise((resolve, reject) => {++ const req = http.request(`${b2}/release-metrics`, { method: 'POST', headers: { 'Content-Type': 'text/plain' } }, (res) => { res.resume(); res.on('end', () => resolve(res.statusCode)); });++ req.on('error', reject);++ req.end(JSON.stringify({ counts: { reloaded: { user: 2 }, deferred: { typing: 1 } } }));++ });++ assert.strictEqual(report, 204);++ const m2 = await request(b2, '/metrics');++ assert.ok(m2.body.includes('release_client_updates_total{outcome="reloaded",reason="user"} 2\n'), m2.body.slice(0, 400));++ assert.ok(m2.body.includes('release_client_updates_total{outcome="deferred",reason="typing"} 1\n'));} finally {child.kill('SIGKILL');}
@@ -50,7 +50,7 @@ server/
@@ -50,7 +50,7 @@ server/
has_result, never params/result/error text/creator), worker (light/heavy lanes, leases, retries,
has_result, never params/result/error text/creator), worker (light/heavy lanes, leases, retries,
cancellation), routes (/api/v2/:app/jobs), types: thumbnail.regenerate,
cancellation), routes (/api/v2/:app/jobs), types: thumbnail.regenerate,
invariant.scan (proposes split/remux), object.split, object.remux
invariant.scan (proposes split/remux), object.split, object.remux
-(openvibe-shared: pinned OpenVibe.Shared v1.0.0 release, installed by npm)
+(openvibe-shared v1.5.1, openvibe-contracts v0.33.0, openvibe-sdk v0.5.0: pinned release tarballs, installed by npm)
scripts/smoke-test.sh end-to-end smoke test (boots a temp instance)
scripts/smoke-test.sh end-to-end smoke test (boots a temp instance)
scripts/backfill-objects.js / reconcile-objects.js / object-invariant.js / no-good-copy-report.js object-model operator tools
scripts/backfill-objects.js / reconcile-objects.js / object-invariant.js / no-good-copy-report.js object-model operator tools
scripts/media-jobs.js list jobs, run the size-invariant scan (dry run by default), approve/cancel proposals
scripts/media-jobs.js list jobs, run the size-invariant scan (dry run by default), approve/cancel proposals
@@ -295,7 +295,9 @@ and checks each point.
@@ -295,7 +295,9 @@ and checks each point.
|---|---|
|---|---|
| `GET /healthz` | liveness: the process answers (unchanged; it checks nothing else) |
| `GET /healthz` | liveness: the process answers (unchanged; it checks nothing else) |
| `GET /api/ready` | readiness from real checks, each with `status`, `required`, `latency_ms`, `checked_at`. **Required** (503 when one fails): `db` (a query against the SQLite database), `storage_vods`, `storage_clips`, `storage_pastes`, `storage_thumbnails`, `storage_files`, `storage_objects` (a probe file is written and removed). **Optional** (listed in `degraded`, still 200): `network_jwks` (user JWTs and service tokens; app keys work without it), `remote_b2` / `remote_r2` (HeadBucket, at most once a minute; present only when the provider is configured), `events_outbox` (backlog over 1000 events). Also `recordings_in_progress`. |
| `GET /api/ready` | readiness from real checks, each with `status`, `required`, `latency_ms`, `checked_at`. **Required** (503 when one fails): `db` (a query against the SQLite database), `storage_vods`, `storage_clips`, `storage_pastes`, `storage_thumbnails`, `storage_files`, `storage_objects` (a probe file is written and removed). **Optional** (listed in `degraded`, still 200): `network_jwks` (user JWTs and service tokens; app keys work without it), `remote_b2` / `remote_r2` (HeadBucket, at most once a minute; present only when the provider is configured), `events_outbox` (backlog over 1000 events). Also `recordings_in_progress`. |
-| `GET /metrics` | Prometheus text (openvibe-shared/metrics) for **direct loopback callers only**; 404 through nginx. HTTP golden signals by route template, process metrics, `release_info`, plus `media_recordings_in_progress`, `media_object_locations{provider,state}`, `media_objects{lifecycle_status}`, `media_events_outbox{status}` (only while the outbox runs). |
+| `GET /metrics` | Prometheus text (openvibe-shared/metrics) for **direct loopback callers only**; 404 through nginx. HTTP golden signals by route template, process metrics, `release_info`, `release_client_updates_total{outcome,reason}`, plus `media_recordings_in_progress`, `media_object_locations{provider,state}`, `media_objects{lifecycle_status}`, `media_events_outbox{status}` (only while the outbox runs). |
+| `GET /release.json` | release manifest (`registry.release-manifest@1` 1.1.0, openvibe-shared/release, ADR-016): deployed commit, package versions, components, contract ranges, `metrics_url`. The shared navbar's release-watch polls it. No components are declared, so every release still prompts open tabs to reload. |
+| `POST /release-metrics` | open tabs' update reports (release-watch beacons, at most 4 KB; 30 a minute per connecting address, which behind nginx is the proxy, so one budget for all tabs; Sec-GPC/DNT dropped) into `release_client_updates_total` on `/metrics`. 403 in a restore drill, like every write. |
## Public serving (no auth unless the item is private)
## Public serving (no auth unless the item is private)
@@ -16,9 +16,9 @@
@@ -16,9 +16,9 @@
"dotenv": "^16.4.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express": "^4.19.2",
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.28.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"sharp": "^0.33.4"
"sharp": "^0.33.4"
}
}
},
},
@@ -1919,9 +1919,9 @@
@@ -1919,9 +1919,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.28.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.28.0",
- "integrity": "sha512-cOOEmIhcjDmnWbo4jsz6kYOKT+Kc+aitYoc4w/EJOKg5uvJDt10vctjKB3IIl53UXeuDOhbckP53yBdBeewC1A==",
+ "version": "0.33.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1935,17 +1935,18 @@
@@ -1935,17 +1935,18 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"node_modules/openvibe-sdk": {
- "version": "0.2.2",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",
- "integrity": "sha512-kDifUp3EcmnTWzElAmaCZnX659WErcnm4+qVEoAiYQWX/w/1p4TWvoHbUPsrX65TSROVR3tiyVkl801n/0yyCw==",
+ "version": "0.5.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
+ "integrity": "sha512-QVJkR5iazQX0ZwIrDp2A88ZeYYBSXEmSzhpgURPUSnzezIM3jz0HBhVKowyhkvg2aBO61XNf77eb4zJGkMjZiw==",
"license": "MIT",
"license": "MIT",
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.3.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
+ "version": "1.5.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
+ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -11,8 +11,8 @@
@@ -11,8 +11,8 @@
},
},
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.28.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
"@aws-sdk/client-s3": "^3.600.0",
"@aws-sdk/client-s3": "^3.600.0",
"@aws-sdk/lib-storage": "^3.600.0",
"@aws-sdk/lib-storage": "^3.600.0",
"@aws-sdk/s3-request-presigner": "^3.600.0",
"@aws-sdk/s3-request-presigner": "^3.600.0",
@@ -20,7 +20,7 @@
@@ -20,7 +20,7 @@
"dotenv": "^16.4.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express": "^4.19.2",
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"sharp": "^0.33.4"
"sharp": "^0.33.4"
}
}
}
}
@@ -114,8 +114,10 @@ app.options(['/api/v1/:app/*', '/api/v2/:app/*'], (req, res) => {
@@ -114,8 +114,10 @@ app.options(['/api/v1/:app/*', '/api/v2/:app/*'], (req, res) => {
// ── Routes ───────────────────────────────────────────────────
// ── Routes ───────────────────────────────────────────────────
-// What this server runs (ADR-016); the shared navbar's release-watch polls it.
-app.get('/release.json', release.handler);
+// What this server runs (ADR-016); the shared navbar's release-watch polls it. POST /release-metrics
+// takes open tabs' update reports into /metrics (release_client_updates_total). A restore drill
+// answers that POST with the readOnly 403 above, like every other write.
+release.mount(app, { registry: instrumented.registry });
app.get('/healthz', (req, res) => {
app.get('/healthz', (req, res) => {
res.json({
res.json({
@@ -14,7 +14,7 @@
@@ -14,7 +14,7 @@
* - The endpoints the Host inventory compares (/release.json, the /browse index) and the watch pages
* - The endpoints the Host inventory compares (/release.json, the /browse index) and the watch pages
* render from the copy; every route that would stream a stored file answers 503 without opening
* render from the copy; every route that would stream a stored file answers 503 without opening
* the path the database gives it.
* the path the database gives it.
- * - Writes answer 403, sign-in 503, and the copy's rows stay as they were.
+ * - Writes answer 403 (POST /release-metrics too), sign-in 503, and the copy's rows stay as they were.
*
*
* Run: node test/drill-mode.test.js
* Run: node test/drill-mode.test.js
*/
*/
@@ -271,6 +271,7 @@ const safe = { DB_PATH, HOST: '127.0.0.1', PORT: '14100' };
@@ -271,6 +271,7 @@ const safe = { DB_PATH, HOST: '127.0.0.1', PORT: '14100' };
['POST', '/api/v1/live/vods', { title: 'x' }], ['PUT', '/api/v1/live/vods/1', { title: 'y' }], ['DELETE', '/api/v1/live/vods/1', null],
['POST', '/api/v1/live/vods', { title: 'x' }], ['PUT', '/api/v1/live/vods/1', { title: 'y' }], ['DELETE', '/api/v1/live/vods/1', null],
['POST', '/api/v1/live/clips', { vod_id: 1 }], ['PUT', `/api/v2/live/objects/${objectId}/content`, 'raw bytes'], ['POST', '/api/v2/live/jobs', { type: 'thumbnail' }],
['POST', '/api/v1/live/clips', { vod_id: 1 }], ['PUT', `/api/v2/live/objects/${objectId}/content`, 'raw bytes'], ['POST', '/api/v2/live/jobs', { type: 'thumbnail' }],
['POST', '/api/v1/live/admin/storage/tiers/sweep', {}], ['POST', '/internal/avatar-ingest', {}], ['PATCH', '/api/v1/live/vods/1', {}],
['POST', '/api/v1/live/admin/storage/tiers/sweep', {}], ['POST', '/internal/avatar-ingest', {}], ['PATCH', '/api/v1/live/vods/1', {}],
+ ['POST', '/release-metrics', { counts: { reloaded: { user: 1 } } }],
];
];
for (const [m, p, body] of writes) {
for (const [m, p, body] of writes) {
const r = await request(m, p, { headers: auth, body });
const r = await request(m, p, { headers: auth, body });
@@ -146,6 +146,24 @@ const listen = (app) => new Promise((resolve) => { const s = app.listen(0, '127.
@@ -146,6 +146,24 @@ const listen = (app) => new Promise((resolve) => { const s = app.listen(0, '127.
assert.ok(m.body.includes('http_requests_total{method="GET",route="/api/ready",status_class="2xx"} 1\n'), m.body.slice(0, 400));
assert.ok(m.body.includes('http_requests_total{method="GET",route="/api/ready",status_class="2xx"} 1\n'), m.body.slice(0, 400));
const h = await request(b2, '/healthz');
const h = await request(b2, '/healthz');
assert.strictEqual(h.status, 200, '/healthz still answers');
assert.strictEqual(h.status, 200, '/healthz still answers');
+ // The release manifest (registry.release-manifest@1) names where open tabs report updates,
+ // and those reports land in /metrics.
+ const rel = await request(b2, '/release.json');
+ assert.strictEqual(rel.status, 200);
+ const manifest = JSON.parse(rel.body);
+ assert.strictEqual(manifest.service, 'media');
+ assert.strictEqual(manifest.release, 'abcdef123456');
+ assert.deepStrictEqual(require('openvibe-contracts').validate('registry.release-manifest@1', manifest).errors, []);
+ assert.strictEqual(manifest.metrics_url, '/release-metrics');
+ const report = await new Promise((resolve, reject) => {
+ const req = http.request(`${b2}/release-metrics`, { method: 'POST', headers: { 'Content-Type': 'text/plain' } }, (res) => { res.resume(); res.on('end', () => resolve(res.statusCode)); });
+ req.on('error', reject);
+ req.end(JSON.stringify({ counts: { reloaded: { user: 2 }, deferred: { typing: 1 } } }));
+ });
+ assert.strictEqual(report, 204);
+ const m2 = await request(b2, '/metrics');
+ assert.ok(m2.body.includes('release_client_updates_total{outcome="reloaded",reason="user"} 2\n'), m2.body.slice(0, 400));
+ assert.ok(m2.body.includes('release_client_updates_total{outcome="deferred",reason="typing"} 1\n'));
} finally {
} finally {
child.kill('SIGKILL');
child.kill('SIGKILL');
}
}