mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:41:08 +02:00
chore(diagnostics): headless prod smoke scripts
Some checks failed
CD Mac Mini / Detect Changes (push) Has been cancelled
CI / Detect Changes (push) Has been cancelled
CI / Validate (push) Has been cancelled
CI / Auth flow integration test (push) Has been cancelled
Docker Validate / Validate Dockerfiles (push) Has been cancelled
Mirror to Forgejo / Push to Forgejo (push) Has been cancelled
CD Mac Mini / Deploy (push) Has been cancelled
CI / Build mana-auth (push) Has been cancelled
CI / Build mana-search (push) Has been cancelled
CI / Build mana-sync (push) Has been cancelled
CI / Build mana-notify (push) Has been cancelled
CI / Build mana-api-gateway (push) Has been cancelled
CI / Build mana-crawler (push) Has been cancelled
CI / Build mana-media (push) Has been cancelled
CI / Build mana-credits (push) Has been cancelled
CI / Build mana-web (push) Has been cancelled
CI / Build chat-backend (push) Has been cancelled
CI / Build chat-web (push) Has been cancelled
CI / Build todo-backend (push) Has been cancelled
CI / Build todo-web (push) Has been cancelled
CI / Build calendar-backend (push) Has been cancelled
CI / Build calendar-web (push) Has been cancelled
CI / Build clock-web (push) Has been cancelled
CI / Build contacts-backend (push) Has been cancelled
CI / Build contacts-web (push) Has been cancelled
CI / Build presi-web (push) Has been cancelled
CI / Build storage-backend (push) Has been cancelled
CI / Build storage-web (push) Has been cancelled
CI / Build telegram-stats-bot (push) Has been cancelled
CI / Build food-backend (push) Has been cancelled
CI / Build food-web (push) Has been cancelled
CI / Build skilltree-web (push) Has been cancelled
Docker Validate / Build calendar-web (push) Has been cancelled
Docker Validate / Build quotes-web (push) Has been cancelled
Docker Validate / Build todo-backend (push) Has been cancelled
Docker Validate / Build todo-web (push) Has been cancelled
Docker Validate / Build mana-auth (push) Has been cancelled
Docker Validate / Build mana-sync (push) Has been cancelled
Docker Validate / Build mana-media (push) Has been cancelled
Some checks failed
CD Mac Mini / Detect Changes (push) Has been cancelled
CI / Detect Changes (push) Has been cancelled
CI / Validate (push) Has been cancelled
CI / Auth flow integration test (push) Has been cancelled
Docker Validate / Validate Dockerfiles (push) Has been cancelled
Mirror to Forgejo / Push to Forgejo (push) Has been cancelled
CD Mac Mini / Deploy (push) Has been cancelled
CI / Build mana-auth (push) Has been cancelled
CI / Build mana-search (push) Has been cancelled
CI / Build mana-sync (push) Has been cancelled
CI / Build mana-notify (push) Has been cancelled
CI / Build mana-api-gateway (push) Has been cancelled
CI / Build mana-crawler (push) Has been cancelled
CI / Build mana-media (push) Has been cancelled
CI / Build mana-credits (push) Has been cancelled
CI / Build mana-web (push) Has been cancelled
CI / Build chat-backend (push) Has been cancelled
CI / Build chat-web (push) Has been cancelled
CI / Build todo-backend (push) Has been cancelled
CI / Build todo-web (push) Has been cancelled
CI / Build calendar-backend (push) Has been cancelled
CI / Build calendar-web (push) Has been cancelled
CI / Build clock-web (push) Has been cancelled
CI / Build contacts-backend (push) Has been cancelled
CI / Build contacts-web (push) Has been cancelled
CI / Build presi-web (push) Has been cancelled
CI / Build storage-backend (push) Has been cancelled
CI / Build storage-web (push) Has been cancelled
CI / Build telegram-stats-bot (push) Has been cancelled
CI / Build food-backend (push) Has been cancelled
CI / Build food-web (push) Has been cancelled
CI / Build skilltree-web (push) Has been cancelled
Docker Validate / Build calendar-web (push) Has been cancelled
Docker Validate / Build quotes-web (push) Has been cancelled
Docker Validate / Build todo-backend (push) Has been cancelled
Docker Validate / Build todo-web (push) Has been cancelled
Docker Validate / Build mana-auth (push) Has been cancelled
Docker Validate / Build mana-sync (push) Has been cancelled
Docker Validate / Build mana-media (push) Has been cancelled
Two Playwright-based diagnostic scripts for investigating
production-only browser issues that curl can't reproduce:
- scripts/smoke-prod.mjs: loads mana.how like a fresh incognito
tab, waits a configurable budget, reports every console error,
request failure, still-pending request, and slow resource.
- scripts/smoke-prod-load.mjs: measures DOMContentLoaded + load
event timing explicitly. Distinguishes "app interactive" from
"browser tab spinner stops".
Run: `node apps/mana/apps/web/scripts/smoke-prod.mjs`
MANA_URL=https://mana.how/login MANA_WAIT_MS=45000 node ...
Used today to rule out server-side issues in a loader-hang report
that reproduced only in one specific browser profile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
16c8818338
commit
32c95a3780
2 changed files with 258 additions and 0 deletions
100
apps/mana/apps/web/scripts/smoke-prod-load.mjs
Normal file
100
apps/mana/apps/web/scripts/smoke-prod-load.mjs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Same as smoke-prod.mjs but explicitly waits for the 'load' event — the
|
||||
// one that controls whether Chrome's tab spinner keeps spinning. If the
|
||||
// tab spinner hangs in a real browser but Playwright's .goto({waitUntil:
|
||||
// 'load'}) resolves quickly, the hang isn't about 'load' — it's about
|
||||
// some async op kicked off after load that browsers still count as
|
||||
// "loading" (e.g. service-worker install, web-manifest sub-fetches,
|
||||
// deferred analytics).
|
||||
//
|
||||
// Also dumps ALL non-200 or slow requests to see which ones the browser
|
||||
// is actually waiting on.
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const URL = process.env.MANA_URL ?? 'https://mana.how/';
|
||||
const GOTO_TIMEOUT = Number(process.env.MANA_GOTO_TIMEOUT ?? 60_000);
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 800 },
|
||||
userAgent:
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0 Safari/537.36',
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
const requests = new Map();
|
||||
const errors = [];
|
||||
|
||||
page.on('request', (req) => {
|
||||
requests.set(req.url() + '#' + req.method(), {
|
||||
url: req.url(),
|
||||
method: req.method(),
|
||||
startedAt: Date.now(),
|
||||
resourceType: req.resourceType(),
|
||||
});
|
||||
});
|
||||
page.on('response', (res) => {
|
||||
const k = res.url() + '#' + res.request().method();
|
||||
const e = requests.get(k);
|
||||
if (e) {
|
||||
e.status = res.status();
|
||||
e.duration = Date.now() - e.startedAt;
|
||||
}
|
||||
});
|
||||
page.on('requestfailed', (req) => {
|
||||
const k = req.url() + '#' + req.method();
|
||||
const e = requests.get(k);
|
||||
if (e) {
|
||||
e.failure = req.failure()?.errorText ?? 'unknown';
|
||||
e.duration = Date.now() - e.startedAt;
|
||||
}
|
||||
});
|
||||
page.on('pageerror', (err) => errors.push(err.message));
|
||||
|
||||
const t0 = Date.now();
|
||||
|
||||
let domContentLoadedAt = null;
|
||||
let loadedAt = null;
|
||||
page.on('domcontentloaded', () => (domContentLoadedAt = Date.now() - t0));
|
||||
page.on('load', () => (loadedAt = Date.now() - t0));
|
||||
|
||||
try {
|
||||
console.log(`→ goto ${URL} (waitUntil=load, timeout=${GOTO_TIMEOUT}ms)`);
|
||||
await page.goto(URL, { waitUntil: 'load', timeout: GOTO_TIMEOUT });
|
||||
console.log(`✓ load event fired`);
|
||||
} catch (e) {
|
||||
console.log(`✗ goto failed: ${e.message}`);
|
||||
}
|
||||
|
||||
const gotoEnded = Date.now() - t0;
|
||||
|
||||
// After load, wait up to 10s more to see if anything starts hanging.
|
||||
await page.waitForTimeout(10_000);
|
||||
|
||||
const pending = [...requests.values()].filter((r) => r.status === undefined && !r.failure);
|
||||
const failed = [...requests.values()].filter((r) => r.failure);
|
||||
|
||||
console.log('');
|
||||
console.log(`DOMContentLoaded: ${domContentLoadedAt ?? 'never'} ms`);
|
||||
console.log(`load event: ${loadedAt ?? 'never'} ms`);
|
||||
console.log(`goto returned at: ${gotoEnded} ms`);
|
||||
console.log(`total requests: ${requests.size}`);
|
||||
console.log(`still pending after 10s post-load: ${pending.length}`);
|
||||
if (pending.length) {
|
||||
console.log('');
|
||||
console.log('─── HANGING ───');
|
||||
for (const r of pending) {
|
||||
const age = Date.now() - r.startedAt;
|
||||
console.log(` ${age}ms pending ${r.method} ${r.resourceType} ${r.url}`);
|
||||
}
|
||||
}
|
||||
if (failed.length) {
|
||||
console.log('');
|
||||
console.log('─── FAILED ───');
|
||||
for (const r of failed) console.log(` ${r.failure} ${r.url}`);
|
||||
}
|
||||
console.log('');
|
||||
console.log(`uncaught pageerrors: ${errors.length}`);
|
||||
for (const e of errors) console.log(` ${e}`);
|
||||
|
||||
await browser.close();
|
||||
158
apps/mana/apps/web/scripts/smoke-prod.mjs
Normal file
158
apps/mana/apps/web/scripts/smoke-prod.mjs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// Headless diagnostic: load mana.how like a fresh incognito tab, capture
|
||||
// every signal a browser would expose (console, network, uncaught errors,
|
||||
// pending requests after N seconds, final DOM state). Prints a tight
|
||||
// report — no side effects.
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const URL = process.env.MANA_URL ?? 'https://mana.how/';
|
||||
const WAIT_MS = Number(process.env.MANA_WAIT_MS ?? 30_000);
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
// Fresh context = no cookies, no storage — closest we get to incognito.
|
||||
storageState: undefined,
|
||||
viewport: { width: 1280, height: 800 },
|
||||
userAgent:
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0 Safari/537.36',
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
/** @type {Array<{level: string, text: string, location?: string}>} */
|
||||
const consoleEntries = [];
|
||||
/** @type {Array<{type: string, message: string, stack?: string}>} */
|
||||
const errors = [];
|
||||
/** @type {Map<string, {url: string, method: string, startedAt: number, status?: number, failure?: string, duration?: number, resourceType: string}>} */
|
||||
const requests = new Map();
|
||||
|
||||
page.on('console', (msg) => {
|
||||
consoleEntries.push({
|
||||
level: msg.type(),
|
||||
text: msg.text(),
|
||||
location: msg.location()?.url,
|
||||
});
|
||||
});
|
||||
|
||||
page.on('pageerror', (err) => {
|
||||
errors.push({ type: 'pageerror', message: err.message, stack: err.stack });
|
||||
});
|
||||
|
||||
page.on('crash', () => {
|
||||
errors.push({ type: 'crash', message: 'page crashed' });
|
||||
});
|
||||
|
||||
page.on('request', (req) => {
|
||||
requests.set(req.url() + '#' + req.method(), {
|
||||
url: req.url(),
|
||||
method: req.method(),
|
||||
startedAt: Date.now(),
|
||||
resourceType: req.resourceType(),
|
||||
});
|
||||
});
|
||||
|
||||
page.on('response', async (res) => {
|
||||
const key = res.url() + '#' + res.request().method();
|
||||
const entry = requests.get(key);
|
||||
if (entry) {
|
||||
entry.status = res.status();
|
||||
entry.duration = Date.now() - entry.startedAt;
|
||||
}
|
||||
});
|
||||
|
||||
page.on('requestfailed', (req) => {
|
||||
const key = req.url() + '#' + req.method();
|
||||
const entry = requests.get(key);
|
||||
if (entry) {
|
||||
entry.failure = req.failure()?.errorText ?? 'unknown';
|
||||
entry.duration = Date.now() - entry.startedAt;
|
||||
}
|
||||
});
|
||||
|
||||
const startedAt = Date.now();
|
||||
console.log(`→ loading ${URL} (wait up to ${WAIT_MS / 1000}s)`);
|
||||
|
||||
let navError = null;
|
||||
try {
|
||||
// Don't wait for networkidle — that's precisely what's broken. Just
|
||||
// fire the navigation and let our WAIT_MS budget decide when to look.
|
||||
await page.goto(URL, { waitUntil: 'commit', timeout: 15_000 });
|
||||
} catch (e) {
|
||||
navError = e.message;
|
||||
}
|
||||
|
||||
// Let the page chew on JS for the wait budget, then snapshot.
|
||||
await page.waitForTimeout(WAIT_MS);
|
||||
const elapsed = Date.now() - startedAt;
|
||||
|
||||
const title = await page.title().catch(() => '<title failed>');
|
||||
const bodyText = await page
|
||||
.evaluate(() => document.body?.innerText?.slice(0, 500) ?? '')
|
||||
.catch(() => '<eval failed>');
|
||||
const currentURL = page.url();
|
||||
|
||||
const pending = [...requests.values()].filter((r) => r.status === undefined && !r.failure);
|
||||
const failed = [...requests.values()].filter((r) => r.failure);
|
||||
const slow = [...requests.values()]
|
||||
.filter((r) => r.duration && r.duration > 2000 && r.status !== undefined)
|
||||
.sort((a, b) => (b.duration ?? 0) - (a.duration ?? 0));
|
||||
const byStatus = {};
|
||||
for (const r of requests.values()) {
|
||||
if (r.status !== undefined) byStatus[r.status] = (byStatus[r.status] ?? 0) + 1;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('═══ RESULT ═══');
|
||||
console.log(`elapsed: ${elapsed}ms`);
|
||||
console.log(`final URL: ${currentURL}`);
|
||||
console.log(`title: ${title}`);
|
||||
console.log(`nav error: ${navError ?? 'none'}`);
|
||||
console.log('');
|
||||
|
||||
console.log('─── body preview (first 500 chars) ───');
|
||||
console.log(bodyText || '<empty>');
|
||||
console.log('');
|
||||
|
||||
console.log(`─── requests by status (total ${requests.size}) ───`);
|
||||
for (const [code, count] of Object.entries(byStatus).sort()) {
|
||||
console.log(` ${code}: ${count}`);
|
||||
}
|
||||
if (pending.length) {
|
||||
console.log('');
|
||||
console.log(`─── STILL PENDING after ${WAIT_MS / 1000}s (${pending.length}) ───`);
|
||||
for (const r of pending) {
|
||||
console.log(` ${r.method.padEnd(6)} ${r.resourceType.padEnd(8)} ${r.url}`);
|
||||
}
|
||||
}
|
||||
if (failed.length) {
|
||||
console.log('');
|
||||
console.log(`─── FAILED (${failed.length}) ───`);
|
||||
for (const r of failed) {
|
||||
console.log(` ${r.method.padEnd(6)} ${r.failure?.padEnd(35)} ${r.url}`);
|
||||
}
|
||||
}
|
||||
if (slow.length) {
|
||||
console.log('');
|
||||
console.log(`─── slow (>2s, finished) ───`);
|
||||
for (const r of slow.slice(0, 10)) {
|
||||
console.log(` ${r.duration}ms ${r.status} ${r.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(`─── console messages (${consoleEntries.length}) ───`);
|
||||
for (const msg of consoleEntries) {
|
||||
if (['error', 'warning'].includes(msg.level)) {
|
||||
console.log(` [${msg.level}] ${msg.text}${msg.location ? ' @ ' + msg.location : ''}`);
|
||||
}
|
||||
}
|
||||
const nonWarn = consoleEntries.filter((m) => !['error', 'warning'].includes(m.level));
|
||||
if (nonWarn.length) console.log(` (+${nonWarn.length} log/info/debug suppressed)`);
|
||||
|
||||
console.log('');
|
||||
console.log(`─── uncaught errors (${errors.length}) ───`);
|
||||
for (const e of errors) {
|
||||
console.log(` [${e.type}] ${e.message}`);
|
||||
if (e.stack) console.log(e.stack.split('\n').slice(0, 3).join('\n'));
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
Loading…
Add table
Add a link
Reference in a new issue