Is the business page really working, or are background APIs failing?
A page can return HTTP 200 and still be broken for the business. Data may be missing, buttons may have nothing to act on, or a widget may silently fail because an XHR or fetch request returned an error. This verifier loads the page in a real browser, confirms expected business content, and watches the page's background API traffic for failures.
Configuration
Environment: PAGE_URL, EXPECTED_TEXT
Requires Playwright, its Chromium browser, and Node.js 24 or later. The recipe treats failed fetch/XHR requests and HTTP 4xx/5xx fetch/XHR responses as capability failures.
Recipe structure
The top-level executable entry file sits beside a same-named directory that owns the canonical implementation and its dependency manifest. Run npm install once in that directory and retain the generated package-lock.json:
verifiers/
├── detect-hidden-api-errors.js
└── detect-hidden-api-errors/
├── package.json
└── detect-hidden-api-errors.jsThe canonical contract stays at the boundary.
The implementation exports the canonical verifier object: id, label, schedule.interval, schedule.timeout, and asynchronous verify() returning structured summary and steps. The top-level file exports that exact object unchanged, adds validated standalone execution, and is what BRS-Core discovers.
Top-level executable entry file
verifiers/detect-hidden-api-errors.js
#!/usr/bin/env node
'use strict';
const verifier = require('./detect-hidden-api-errors/detect-hidden-api-errors');
module.exports = verifier;
if (require.main === module) {
void runStandalone(verifier);
}
async function runStandalone(standaloneVerifier) {
let timeoutHandle;
try {
const timeout = standaloneVerifier.schedule?.timeout ?? 30;
if (!Number.isInteger(timeout) || timeout <= 0) {
throw new Error('schedule.timeout must be a positive integer');
}
const timeoutPromise = new Promise((_, reject) => {
timeoutHandle = setTimeout(() => {
const error = new Error(
`Standalone verifier execution timed out after ${timeout} seconds.`
);
error.code = 'BRS_TIMEOUT';
reject(error);
}, timeout * 1000);
});
const result = await Promise.race([
Promise.resolve().then(() => standaloneVerifier.verify()),
timeoutPromise
]);
validateResult(result);
await writeStream(
process.stdout,
`${JSON.stringify(result, null, 2)}\n`
);
process.exit(result.steps.some((step) => !step.success) ? 1 : 0);
} catch (error) {
const summary = error?.code === 'BRS_TIMEOUT'
? error.message
: error?.code === 'BRS_INVALID_RESULT'
? 'Standalone verifier returned a malformed result.'
: 'Standalone verifier execution failed unexpectedly.';
await Promise.all([
writeStream(
process.stdout,
`${JSON.stringify({ summary, steps: [] }, null, 2)}\n`
),
writeStream(process.stderr, `${sanitize(error?.message)}\n`)
]);
process.exit(1);
} finally {
clearTimeout(timeoutHandle);
}
}
function validateResult(result) {
const names = new Set();
const valid = result &&
typeof result === 'object' &&
!Array.isArray(result) &&
typeof result.summary === 'string' &&
result.summary.trim() !== '' &&
Array.isArray(result.steps) &&
result.steps.length > 0 &&
result.steps.every((step) => {
const uniqueName = step &&
/^[a-z][a-z0-9_]*$/.test(step.name) &&
!names.has(step.name);
if (uniqueName) names.add(step.name);
return uniqueName &&
typeof step.label === 'string' &&
step.label.trim() !== '' &&
typeof step.success === 'boolean' &&
Number.isFinite(step.durationSeconds) &&
step.durationSeconds >= 0 &&
typeof step.message === 'string' &&
step.message.trim() !== '' &&
typeof step.checkedAt === 'string' &&
Number.isFinite(Date.parse(step.checkedAt));
}) &&
(result.observations === undefined ||
Array.isArray(result.observations));
if (!valid) {
const error = new Error('verifier result is malformed');
error.code = 'BRS_INVALID_RESULT';
throw error;
}
}
function writeStream(stream, value) {
return new Promise((resolve, reject) => {
stream.write(value, (error) => error ? reject(error) : resolve());
});
}
function sanitize(value) {
return String(value || 'Unknown error')
.split(/\r?\n/, 1)[0]
.replace(/[\u0000-\u001f\u007f]/g, ' ')
.slice(0, 500);
}Verifier implementation
verifiers/detect-hidden-api-errors/detect-hidden-api-errors.js
'use strict';
const { chromium } = require('playwright');
const path = require('node:path');
module.exports = {
id: 'hidden_page_api_errors',
label: 'Hidden Page API Errors',
schedule: {
interval: 300,
timeout: 60
},
async verify() {
loadLocalEnvironment();
const pageUrl = required('PAGE_URL');
const expectedText = required('EXPECTED_TEXT');
const startedAt = Date.now();
const steps = [];
const failures = [];
let browser;
try {
browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
page.on('response', (response) => {
const status = response.status();
if (status >= 400 && isApiResponse(response)) {
failures.push(
`HTTP ${status} ${response.request().method()}`
);
}
});
page.on('requestfailed', (request) => {
if (isApiRequest(request)) {
failures.push(
`FAILED ${request.method()}`
);
}
});
await page.goto(pageUrl, {
waitUntil: 'networkidle'
});
await page.getByText(expectedText, {
exact: false
}).first().waitFor({
state: 'visible',
timeout: 10000
});
if (failures.length > 0) {
throw new Error(
`${failures.length} background API request(s) failed: ` +
failures.slice(0, 3).join(', ')
);
}
steps.push(step(
'verify_page_api_requests',
'Verify Page API Requests',
true,
startedAt,
'The page loaded with no failed background API requests.'
));
} catch (error) {
logFailure('verify_page_api_requests', error);
steps.push(step(
'verify_page_api_requests',
'Verify Page API Requests',
false,
startedAt,
'The page capability has a visible or background API failure.'
));
} finally {
if (browser) await browser.close();
}
return {
summary: steps[0].success
? 'The web capability is working without hidden API failures.'
: 'The web capability has a visible or background API failure.',
steps
};
}
};
function isApiResponse(response) {
return isApiRequest(response.request());
}
function isApiRequest(request) {
const type = request.resourceType();
return type === 'xhr' || type === 'fetch';
}
function loadLocalEnvironment() {
try {
process.loadEnvFile(path.join(__dirname, '.env'));
} catch (error) {
if (!error || error.code !== 'ENOENT') throw error;
}
}
function required(name) {
const value = String(process.env[name] || '').trim();
if (!value) throw new Error(`${name} is required`);
return value;
}
function logFailure(name, error) {
const detail = sanitize(error instanceof Error ? error.message : error);
console.error(
`verifier=hidden_page_api_errors step=${name} error="${detail}"`
);
}
function sanitize(value) {
return String(value || 'Unknown error')
.split(/\r?\n/, 1)[0]
.replace(/[\u0000-\u001f\u007f]/g, ' ')
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.slice(0, 500);
}
function step(name, label, success, startedAt, message) {
return {
name,
label,
success,
durationSeconds: Number(
((Date.now() - startedAt) / 1000).toFixed(3)
),
message,
checkedAt: new Date().toISOString()
};
}Dependency manifest
verifiers/detect-hidden-api-errors/package.json
{
"name": "brs-verifier-hidden-api-errors",
"version": "1.0.0",
"private": true,
"type": "commonjs",
"scripts": {
"verify": "node ../detect-hidden-api-errors.js"
},
"dependencies": {
"playwright": "1.62.1"
},
"engines": {
"node": ">=24"
}
}Run it directly
cd detect-hidden-api-errors npm install npx playwright install chromium npm run verify
A passing run writes validated structured JSON to standard output and exits 0. A business failure includes a failed step and exits 1. A timeout, malformed result, or unexpected execution error writes a runner-generated JSON result and exits 1.
Run it continuously with BRS-Core
This verifier does not require BRS-Core to execute. Direct execution is part of the verifier contract and makes development, troubleshooting, CI/CD use, and manual diagnostics straightforward.
BRS-Core can run this verifier alongside your other approved verifiers continuously as part of 24/7 business capability monitoring. Because every recipe presents the same canonical contract, Core can discover and schedule these verifiers together, run them independently, enforce their timeouts, record their results, and expose their signals to the monitoring environment.