Merge pull request #2002 from rcourtman/maintainer/20260909T090949Z

Make Settings startup failures actionable without weakening checks
This commit is contained in:
pulse-triage[bot]
2026-09-09 09:48:57 +00:00
committed by GitHub
5 changed files with 156 additions and 21 deletions
@@ -0,0 +1,9 @@
import type { Page } from '@playwright/test';
export type BootstrapTiming = {
transport: string;
path: string;
started: number;
status: number | null;
duration: number | null;
};
export function observeBootstrapTiming(page: Page, now?: () => number): () => Promise<BootstrapTiming[]>;
@@ -0,0 +1,23 @@
# Settings bootstrap timing diagnostic
The desktop-to-390px Settings test attaches `bootstrap-paired-timing` to its
Playwright report, including on assertion failure. It issues at most one extra
GET each for summary and runtime capabilities, when the browser first requests
that path. The HTTP client shares the test's cookie session but not the browser's
network scheduler. Redirect following is disabled; each probe has a five-second
limit. No readiness condition, capability requirement or assertion is relaxed.
`started` is host epoch milliseconds. Browser duration ends at response headers;
HTTP duration includes response receipt. Null browser status/duration is censored,
not a server error. Null HTTP status is a transport failure/timeout without a
received response; the exception is deliberately not retained. Reports exclude
headers, credentials, query strings and payloads.
For the next hosted observation, retain this attachment, the browser trace and
application startup timestamps from the same job/source. Compare overlapping
summary/capabilities requests; a quick HTTP response with stalled browser narrows
the question towards browser scheduling, while both delayed supports further
server/startup measurement. Neither proves a particular lock is responsible.
Extra probes can warm caches or contend themselves, so do not claim a controlled
effect size or historical causality. Do not select favourable repeats, widen the
assertion timeout, or treat a successful diagnostic as probation promotion.
@@ -0,0 +1,50 @@
// Bounded paired probes for the Settings bootstrap diagnostic. Never retain
// headers, cookies, bodies, query strings, tenant IDs or exception messages.
const paths = new Set(['/api/state/summary', '/api/license/runtime-capabilities']);
export function observeBootstrapTiming(page, now = Date.now) {
const rows = [];
const pending = [];
const seen = new Set();
const starts = new Map();
const onRequest = (request) => {
const path = new URL(request.url()).pathname;
if (!paths.has(path) || seen.has(path) || request.method() !== 'GET') return;
seen.add(path);
const started = now();
const row = { transport: 'browser', path, started, status: null, duration: null };
rows.push(row);
starts.set(request, row);
// page.request uses the same cookie session without the browser network
// scheduler. This extra request can perturb startup: timings are diagnostic,
// not a controlled effect size or a substitute for the original assertion.
pending.push((async () => {
const probe = { transport: 'http', path, started: now(), status: null, duration: null };
rows.push(probe);
let response;
try {
response = await page.request.get(path, { timeout: 5000, maxRedirects: 0 });
probe.status = response.status();
} catch {
// Null means no observed response, not an HTTP error status.
} finally {
probe.duration = now() - probe.started;
await response?.dispose().catch(() => {});
}
})());
};
const onResponse = (response) => {
const row = starts.get(response.request());
if (!row) return;
row.status = response.status();
row.duration = now() - row.started;
};
page.on('request', onRequest);
page.on('response', onResponse);
return async () => {
page.off('request', onRequest);
page.off('response', onResponse);
await Promise.all(pending);
return rows;
};
}
@@ -0,0 +1,44 @@
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import test from 'node:test';
import { observeBootstrapTiming } from './bootstrap-timing.mjs';
const request = (path) => ({ url: () => `http://localhost${path}`, method: () => 'GET' });
test('pairs each allowed browser path once, with bounded cookie-context HTTP and no payload', async () => {
const page = new EventEmitter();
const calls = [];
let disposed = 0;
page.request = { get: async (...args) => {
calls.push(args);
return { status: () => 200, dispose: async () => { disposed++; } };
} };
let time = 100;
const finish = observeBootstrapTiming(page, () => time++);
page.emit('request', request('/api/state?secret=private'));
const summary = request('/api/state/summary?secret=private');
page.emit('request', summary);
page.emit('request', summary);
page.emit('response', { request: () => summary, status: () => 200 });
page.emit('request', request('/api/license/runtime-capabilities'));
const rows = await finish();
assert.equal(calls.length, 2);
assert.deepEqual(calls[0], ['/api/state/summary', { timeout: 5000, maxRedirects: 0 }]);
assert.equal(disposed, 2);
assert.equal(rows.length, 4);
assert.equal(rows[0].status, 200);
assert.equal(rows[2].status, null); // censored browser request
assert.equal(rows[2].duration, null);
assert.doesNotMatch(JSON.stringify(rows), /secret|private/);
assert.equal(page.listenerCount('request'), 0);
assert.equal(page.listenerCount('response'), 0);
});
test('transport failure is censored and never retains exception text', async () => {
const page = new EventEmitter();
page.request = { get: async () => { throw new Error('credential=private'); } };
const finish = observeBootstrapTiming(page);
page.emit('request', request('/api/state/summary'));
const rows = await finish();
assert.equal(rows[1].status, null);
assert.equal(typeof rows[1].duration, 'number');
assert.doesNotMatch(JSON.stringify(rows), /credential|private/);
});
@@ -1,4 +1,5 @@
import fs from 'node:fs';
import { observeBootstrapTiming } from '../scripts/bootstrap-timing.mjs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { test as base, expect } from '@playwright/test';
@@ -175,30 +176,38 @@ test.describe('Settings shell consistency', () => {
});
}
test('keeps direct Settings content inside a 390px viewport after desktop resize', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('/settings/pulse-intelligence/billing/plan', {
waitUntil: 'domcontentloaded',
});
test('keeps direct Settings content inside a 390px viewport after desktop resize', async ({ page }, testInfo) => {
const finishTiming = observeBootstrapTiming(page);
try {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('/settings/pulse-intelligence/billing/plan', {
waitUntil: 'domcontentloaded',
});
const content = page.locator('[data-settings-content]');
await expect(content).toBeVisible();
await expect(page.getByRole('heading', { level: 1, name: 'Plans & Billing' })).toBeVisible();
const content = page.locator('[data-settings-content]');
await expect(content).toBeVisible();
await expect(page.getByRole('heading', { level: 1, name: 'Plans & Billing' })).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
await page.setViewportSize({ width: 390, height: 844 });
const layout = await content.evaluate((element) => {
const bounds = element.getBoundingClientRect();
return {
animationName: getComputedStyle(element).animationName,
left: bounds.left,
right: bounds.right,
viewportWidth: document.documentElement.clientWidth,
};
});
const layout = await content.evaluate((element) => {
const bounds = element.getBoundingClientRect();
return {
animationName: getComputedStyle(element).animationName,
left: bounds.left,
right: bounds.right,
viewportWidth: document.documentElement.clientWidth,
};
});
expect(layout.animationName).toBe('none');
expect(layout.left).toBeGreaterThanOrEqual(0);
expect(layout.right).toBeLessThanOrEqual(layout.viewportWidth);
expect(layout.animationName).toBe('none');
expect(layout.left).toBeGreaterThanOrEqual(0);
expect(layout.right).toBeLessThanOrEqual(layout.viewportWidth);
} finally {
await testInfo.attach('bootstrap-paired-timing', {
body: JSON.stringify(await finishTiming(), null, 2),
contentType: 'application/json',
});
}
});
});