feat: add desktop/mobile device toggle (User-Agent)

- Segmented Desktop / Mobile button in the form
- Desktop: Chrome 124 on Windows
- Mobile: iPhone Safari 17 (iOS 17.4)
- User-Agent injected into k6 script headers
- Stored in DB with migration for existing data
- History items show device pill alongside cache/gzip

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 20:13:13 +02:00
parent 6ef7564e87
commit cf51a4620b
4 changed files with 72 additions and 8 deletions

View File

@@ -27,6 +27,7 @@ db.exec(`
headers TEXT,
cache_mode TEXT NOT NULL DEFAULT 'normal',
gzip INTEGER NOT NULL DEFAULT 1,
device TEXT NOT NULL DEFAULT 'desktop',
status TEXT NOT NULL DEFAULT 'pending',
created_at INTEGER NOT NULL,
finished_at INTEGER,
@@ -38,19 +39,28 @@ db.exec(`
// Non-destructive migration for existing DBs
try { db.exec(`ALTER TABLE tests ADD COLUMN cache_mode TEXT NOT NULL DEFAULT 'normal'`); } catch {}
try { db.exec(`ALTER TABLE tests ADD COLUMN gzip INTEGER NOT NULL DEFAULT 1`); } catch {}
try { db.exec(`ALTER TABLE tests ADD COLUMN device TEXT NOT NULL DEFAULT 'desktop'`); } catch {}
// Stream active test outputs (testId -> {process, clients})
const activeTests = {};
const USER_AGENTS = {
desktop: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
mobile: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1',
};
// Generate a k6 script from test params
function generateScript(params) {
const { url, vus, duration, rpsLimit, httpMethod, requestBody, headers, cacheMode, gzip } = params;
const { url, vus, duration, rpsLimit, httpMethod, requestBody, headers, cacheMode, gzip, device } = params;
// Build the merged headers object
const userHeaders = (() => {
try { return JSON.parse(headers || '{}'); } catch { return {}; }
})();
// User-Agent
userHeaders['User-Agent'] = USER_AGENTS[device] || USER_AGENTS.desktop;
// Cache control headers
if (cacheMode === 'no-cache' || cacheMode === 'bust') {
userHeaders['Cache-Control'] = 'no-cache, no-store';
@@ -136,6 +146,7 @@ app.post('/api/tests', (req, res) => {
headers = '{}',
cacheMode = 'normal',
gzip = true,
device = 'desktop',
} = req.body;
if (!url || !url.startsWith('http')) {
@@ -146,11 +157,11 @@ app.post('/api/tests', (req, res) => {
const createdAt = Date.now();
db.prepare(`
INSERT INTO tests (id, url, vus, duration, rps_limit, http_method, request_body, headers, cache_mode, gzip, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?)
`).run(id, url, vus, duration, rpsLimit || null, httpMethod, requestBody, headers, cacheMode, gzip ? 1 : 0, createdAt);
INSERT INTO tests (id, url, vus, duration, rps_limit, http_method, request_body, headers, cache_mode, gzip, device, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', ?)
`).run(id, url, vus, duration, rpsLimit || null, httpMethod, requestBody, headers, cacheMode, gzip ? 1 : 0, device, createdAt);
const script = generateScript({ url, vus, duration, rpsLimit, httpMethod, requestBody, headers, cacheMode, gzip });
const script = generateScript({ url, vus, duration, rpsLimit, httpMethod, requestBody, headers, cacheMode, gzip, device });
const tmpScript = path.join(os.tmpdir(), `k6-script-${id}.js`);
fs.writeFileSync(tmpScript, script);
@@ -234,7 +245,7 @@ app.get('/api/tests/:id/stream', (req, res) => {
// GET /api/tests — list all tests
app.get('/api/tests', (req, res) => {
const rows = db.prepare('SELECT id, url, vus, duration, rps_limit, http_method, cache_mode, gzip, status, created_at, finished_at, summary FROM tests ORDER BY created_at DESC LIMIT 50').all();
const rows = db.prepare('SELECT id, url, vus, duration, rps_limit, http_method, cache_mode, gzip, device, status, created_at, finished_at, summary FROM tests ORDER BY created_at DESC LIMIT 50').all();
res.json(rows.map(r => ({ ...r, summary: r.summary ? JSON.parse(r.summary) : null })));
});