Files
goodgo-platform/load-tests/scripts/search.js
Ho Ngoc Hai a8e1a438b9 feat(load-tests): add K6 load testing suite for critical API paths
K6 scripts for 4 critical paths:
- Auth (100 VU): login, register, profile
- Listings (500 VU): search with filters, detail view
- Search (200 VU): full-text + geo search
- Payments (50 VU): create payment, list transactions

SLA thresholds: p50<200ms, p95<500ms, p99<1s, error<1%.
CI: manual workflow_dispatch with suite selector.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 08:41:15 +07:00

97 lines
2.7 KiB
JavaScript

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend } from 'k6/metrics';
import { BASE_URL, SLA_THRESHOLDS } from '../lib/config.js';
const textSearchDuration = new Trend('text_search_duration', true);
const geoSearchDuration = new Trend('geo_search_duration', true);
export const options = {
scenarios: {
search_load: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '30s', target: 50 },
{ duration: '2m', target: 200 }, // peak: 200 concurrent
{ duration: '30s', target: 0 },
],
gracefulRampDown: '10s',
},
},
thresholds: {
...SLA_THRESHOLDS,
text_search_duration: ['p(95)<500'],
geo_search_duration: ['p(95)<500'],
},
};
const TEXT_QUERIES = [
'căn hộ',
'nhà phố quận 1',
'đất nền',
'chung cư',
'biệt thự',
'apartment Ho Chi Minh',
'house Hanoi',
];
const GEO_SEARCHES = [
{ lat: 10.7769, lng: 106.7009, radius: 5000 }, // HCM center
{ lat: 21.0285, lng: 105.8542, radius: 5000 }, // Hanoi center
{ lat: 16.0544, lng: 108.2022, radius: 10000 }, // Da Nang
{ lat: 10.8231, lng: 106.6297, radius: 3000 }, // HCM Tan Binh
{ lat: 21.0067, lng: 105.8400, radius: 2000 }, // Hanoi Ba Dinh
];
export default function () {
const iter = __ITER;
if (iter % 2 === 0) {
// Full-text search
const query = TEXT_QUERIES[iter % TEXT_QUERIES.length];
const params = new URLSearchParams({
q: query,
limit: '20',
offset: '0',
});
const res = http.get(`${BASE_URL}/search?${params.toString()}`, {
tags: { name: 'GET /search (text)' },
});
textSearchDuration.add(res.timings.duration);
check(res, {
'text search: status 200|503': (r) => r.status === 200 || r.status === 503,
'text search: valid response': (r) => {
if (r.status === 503) return true; // Typesense unavailable — acceptable
try { return JSON.parse(r.body).data !== undefined; } catch { return false; }
},
});
} else {
// Geo search
const geo = GEO_SEARCHES[iter % GEO_SEARCHES.length];
const params = new URLSearchParams({
lat: String(geo.lat),
lng: String(geo.lng),
radius: String(geo.radius),
limit: '20',
});
const res = http.get(`${BASE_URL}/search/geo?${params.toString()}`, {
tags: { name: 'GET /search/geo' },
});
geoSearchDuration.add(res.timings.duration);
check(res, {
'geo search: status 200|503': (r) => r.status === 200 || r.status === 503,
'geo search: valid response': (r) => {
if (r.status === 503) return true;
try { return JSON.parse(r.body).data !== undefined; } catch { return false; }
},
});
}
sleep(Math.random() * 1 + 0.5);
}