openvibers/openvibe.report Commits
The shadow job compares one Network answer, taken in first (REP-2) (#91)
Changes
@@@ -32,7 +32,7 @@ async function start() {const { app, ctx } = await createApp();const { config } = ctx;-- const jobs = createAnalyticsJobs({ config, analytics: ctx.analytics, shadow: ctx.shadow, log: ctx.log });++ const jobs = createAnalyticsJobs({ config, analytics: ctx.analytics, shadow: ctx.shadow, network: ctx.network, log: ctx.log });// The AI meter's job (plan L40/L41): the daily roll-ups, off under drill mode.const aiUsageJobs = createAiUsageJobs({ config, store: ctx.store, now: ctx.now, log: ctx.log });const allJobs = { ...jobs, stop: async () => { jobs.stop(); aiUsageJobs.stop(); } };@@@ -4,12 +4,14 @@* The two background jobs (server/index.js starts them; tests never do):** ingest pull Network's open stats into Report's tables every REPORT_INGEST_INTERVAL_MS and at boot-- * shadow compare the same window from Network and Report every REPORT_SHADOW_INTERVAL_MS and at boot++ * shadow compare the same window from Network and Report every REPORT_SHADOW_INTERVAL_MS and at boot: one++ * Network answer is taken in and compared, so a rolling number that moved between two reads (the last++ * 24 hours' requests, today's pageviews) is not a difference** Neither job runs when REPORT_JOBS=0. A failed pass logs one short line and leaves the last good data in place;* the next pass retries. Overlapping passes in one process are skipped.*/--function createAnalyticsJobs({ config = {}, analytics = null, shadow = null, log = console } = {}) {++function createAnalyticsJobs({ config = {}, analytics = null, shadow = null, network = null, log = console } = {}) {const cfg = config.jobs || {};const timers = [];let ingestBusy = false;@@@ -33,7 +35,17 @@ function createAnalyticsJobs({ config = {}, analytics = null, shadow = null, logif (!shadow || shadowBusy) return null;shadowBusy = true;try {-- const out = await shadow.compare({ days: cfg.shadowWindowDays || 7 });++ const days = cfg.shadowWindowDays || 7;++ // One Network answer for both sides: taken in first, then compared. Two reads minutes apart never agree on the++ // rolling numbers, so most passes were "differences" that were only the clock (11-03: 382 of 579 passes in a++ // week, 46-58 numbers each, while the equal passes fell between them). A number Report does not reproduce from++ // that same answer is still a difference.++ let stats;++ if (network && typeof network.stats === 'function' && analytics) {++ stats = await network.stats(days);++ if (stats) await analytics.ingestStats(stats);++ }++ const out = await shadow.compare(stats ? { days, stats } : { days });if (out && out.status === 'differences') log.warn(`[OpenVibe.Report] shadow: ${out.differences} difference(s) over ${cfg.shadowWindowDays || 7} days`);return out;} catch (err) {@@@ -43,6 +43,18 @@ const { stats, NOW } = require('./fixtures/analytics');assert.strictEqual(body.diffs[0].metric, 'pageviews');});++ await check('the job compares the one Network answer it took in: a number that moved between two reads is no difference', async () => {++ const { createAnalyticsJobs } = require('../server/jobs/analytics');++ let reads = 0;++ // A rolling number moves on every read, as the last 24 hours' requests and today's pageviews do.++ const network = { stats: async () => { reads++; const s = JSON.parse(JSON.stringify(stats)); s.totals.pageviews = 300 + reads * 7; return s; } };++ const jobs = createAnalyticsJobs({ config: { jobs: { shadowWindowDays: 7 } }, analytics: t.ctx.analytics, shadow: t.ctx.shadow, network, log: { warn() {}, error() {} } });++ const run = await jobs.runShadow();++ assert.strictEqual(run.status, 'equal', JSON.stringify(run.diffs));++ assert.strictEqual(reads, 1, 'one read of Network serves both sides');++ assert.strictEqual(await t.ctx.store.db.value(`SELECT value::int AS v FROM report_metric_windows WHERE metric = 'pageviews' AND scope = 'network' AND window_days = 7`), 307, 'and Report took it in');++ });++await check('a Network that does not answer is a run with no differences, never a clean claim', async () => {const down = await t.ctx.shadow.compare({ days: 7, stats: null });assert.strictEqual(down.status, 'network_unavailable');
@@ -32,7 +32,7 @@ async function start() {
@@ -32,7 +32,7 @@ async function start() {
const { app, ctx } = await createApp();
const { app, ctx } = await createApp();
const { config } = ctx;
const { config } = ctx;
- const jobs = createAnalyticsJobs({ config, analytics: ctx.analytics, shadow: ctx.shadow, log: ctx.log });
+ const jobs = createAnalyticsJobs({ config, analytics: ctx.analytics, shadow: ctx.shadow, network: ctx.network, log: ctx.log });
// The AI meter's job (plan L40/L41): the daily roll-ups, off under drill mode.
// The AI meter's job (plan L40/L41): the daily roll-ups, off under drill mode.
const aiUsageJobs = createAiUsageJobs({ config, store: ctx.store, now: ctx.now, log: ctx.log });
const aiUsageJobs = createAiUsageJobs({ config, store: ctx.store, now: ctx.now, log: ctx.log });
const allJobs = { ...jobs, stop: async () => { jobs.stop(); aiUsageJobs.stop(); } };
const allJobs = { ...jobs, stop: async () => { jobs.stop(); aiUsageJobs.stop(); } };
@@ -4,12 +4,14 @@
@@ -4,12 +4,14 @@
* The two background jobs (server/index.js starts them; tests never do):
* The two background jobs (server/index.js starts them; tests never do):
*
*
* ingest pull Network's open stats into Report's tables every REPORT_INGEST_INTERVAL_MS and at boot
* ingest pull Network's open stats into Report's tables every REPORT_INGEST_INTERVAL_MS and at boot
- * shadow compare the same window from Network and Report every REPORT_SHADOW_INTERVAL_MS and at boot
+ * shadow compare the same window from Network and Report every REPORT_SHADOW_INTERVAL_MS and at boot: one
+ * Network answer is taken in and compared, so a rolling number that moved between two reads (the last
+ * 24 hours' requests, today's pageviews) is not a difference
*
*
* Neither job runs when REPORT_JOBS=0. A failed pass logs one short line and leaves the last good data in place;
* Neither job runs when REPORT_JOBS=0. A failed pass logs one short line and leaves the last good data in place;
* the next pass retries. Overlapping passes in one process are skipped.
* the next pass retries. Overlapping passes in one process are skipped.
*/
*/
-function createAnalyticsJobs({ config = {}, analytics = null, shadow = null, log = console } = {}) {
+function createAnalyticsJobs({ config = {}, analytics = null, shadow = null, network = null, log = console } = {}) {
const cfg = config.jobs || {};
const cfg = config.jobs || {};
const timers = [];
const timers = [];
let ingestBusy = false;
let ingestBusy = false;
@@ -33,7 +35,17 @@ function createAnalyticsJobs({ config = {}, analytics = null, shadow = null, log
@@ -33,7 +35,17 @@ function createAnalyticsJobs({ config = {}, analytics = null, shadow = null, log
if (!shadow || shadowBusy) return null;
if (!shadow || shadowBusy) return null;
shadowBusy = true;
shadowBusy = true;
try {
try {
- const out = await shadow.compare({ days: cfg.shadowWindowDays || 7 });
+ const days = cfg.shadowWindowDays || 7;
+ // One Network answer for both sides: taken in first, then compared. Two reads minutes apart never agree on the
+ // rolling numbers, so most passes were "differences" that were only the clock (11-03: 382 of 579 passes in a
+ // week, 46-58 numbers each, while the equal passes fell between them). A number Report does not reproduce from
+ // that same answer is still a difference.
+ let stats;
+ if (network && typeof network.stats === 'function' && analytics) {
+ stats = await network.stats(days);
+ if (stats) await analytics.ingestStats(stats);
+ }
+ const out = await shadow.compare(stats ? { days, stats } : { days });
if (out && out.status === 'differences') log.warn(`[OpenVibe.Report] shadow: ${out.differences} difference(s) over ${cfg.shadowWindowDays || 7} days`);
if (out && out.status === 'differences') log.warn(`[OpenVibe.Report] shadow: ${out.differences} difference(s) over ${cfg.shadowWindowDays || 7} days`);
return out;
return out;
} catch (err) {
} catch (err) {
@@ -43,6 +43,18 @@ const { stats, NOW } = require('./fixtures/analytics');
@@ -43,6 +43,18 @@ const { stats, NOW } = require('./fixtures/analytics');
assert.strictEqual(body.diffs[0].metric, 'pageviews');
assert.strictEqual(body.diffs[0].metric, 'pageviews');
});
});
+ await check('the job compares the one Network answer it took in: a number that moved between two reads is no difference', async () => {
+ const { createAnalyticsJobs } = require('../server/jobs/analytics');
+ let reads = 0;
+ // A rolling number moves on every read, as the last 24 hours' requests and today's pageviews do.
+ const network = { stats: async () => { reads++; const s = JSON.parse(JSON.stringify(stats)); s.totals.pageviews = 300 + reads * 7; return s; } };
+ const jobs = createAnalyticsJobs({ config: { jobs: { shadowWindowDays: 7 } }, analytics: t.ctx.analytics, shadow: t.ctx.shadow, network, log: { warn() {}, error() {} } });
+ const run = await jobs.runShadow();
+ assert.strictEqual(run.status, 'equal', JSON.stringify(run.diffs));
+ assert.strictEqual(reads, 1, 'one read of Network serves both sides');
+ assert.strictEqual(await t.ctx.store.db.value(`SELECT value::int AS v FROM report_metric_windows WHERE metric = 'pageviews' AND scope = 'network' AND window_days = 7`), 307, 'and Report took it in');
+ });
+
await check('a Network that does not answer is a run with no differences, never a clean claim', async () => {
await check('a Network that does not answer is a run with no differences, never a clean claim', async () => {
const down = await t.ctx.shadow.compare({ days: 7, stats: null });
const down = await t.ctx.shadow.compare({ days: 7, stats: null });
assert.strictEqual(down.status, 'network_unavailable');
assert.strictEqual(down.status, 'network_unavailable');