import http from "k6/http"; import { check, sleep } from "k6"; import { Counter, Rate, Trend } from "k6/metrics"; const BASE_URL = (__ENV.BASE_URL || "http://host.docker.internal").replace( /\/$/, "", ); const API_URL = `${BASE_URL}/api/v1`; const FLAG_KEY = __ENV.FLAG_KEY || ""; const SUBJECT_PREFIX = __ENV.SUBJECT_PREFIX || "k6_subject"; const SUBJECT_COUNTRY = __ENV.SUBJECT_COUNTRY || "US"; const SUBJECT_POOL = Number(__ENV.SUBJECT_POOL || "20000"); const THINK_TIME_SECONDS = Number(__ENV.THINK_TIME_SECONDS || "0"); const START_RATE = Number(__ENV.START_RPS || "20"); const RAMP_UP_RATE = Number(__ENV.RAMP_UP_RPS || "200"); const HOLD_RATE = Number(__ENV.HOLD_RPS || "200"); const PRE_ALLOCATED_VUS = Number(__ENV.PRE_ALLOCATED_VUS || "100"); const MAX_VUS = Number(__ENV.MAX_VUS || "600"); const RAMP_UP_DURATION = __ENV.RAMP_UP_DURATION || "30s"; const HOLD_DURATION = __ENV.HOLD_DURATION || "2m"; const RAMP_DOWN_DURATION = __ENV.RAMP_DOWN_DURATION || "20s"; const THRESHOLD_ERROR_RATE = __ENV.THRESHOLD_ERROR_RATE || "0.01"; const THRESHOLD_P95_MS = __ENV.THRESHOLD_P95_MS || "250"; const THRESHOLD_P99_MS = __ENV.THRESHOLD_P99_MS || "500"; if (!FLAG_KEY) { throw new Error("FLAG_KEY is required"); } export const options = { scenarios: { decide_hot_path: { executor: "ramping-arrival-rate", startRate: START_RATE, timeUnit: "1s", preAllocatedVUs: PRE_ALLOCATED_VUS, maxVUs: MAX_VUS, stages: [ { target: RAMP_UP_RATE, duration: RAMP_UP_DURATION }, { target: HOLD_RATE, duration: HOLD_DURATION }, { target: 0, duration: RAMP_DOWN_DURATION }, ], }, }, thresholds: { http_req_failed: [`rate<${THRESHOLD_ERROR_RATE}`], http_req_duration: [ `p(95)<${THRESHOLD_P95_MS}`, `p(99)<${THRESHOLD_P99_MS}`, ], decide_status_200_rate: ["rate>0.99"], }, summaryTrendStats: [ "avg", "min", "med", "p(90)", "p(95)", "p(99)", "max", ], }; const decideStatus200Rate = new Rate("decide_status_200_rate"); const decideAssignedRate = new Rate("decide_experiment_assigned_rate"); const decideRequests = new Counter("decide_requests_total"); const decideDuration = new Trend("decide_request_duration_ms", true); function buildSubjectId() { const idx = ((__ITER * 104729 + __VU * 8191) % SUBJECT_POOL) + 1; return `${SUBJECT_PREFIX}_${idx}`; } function buildPayload() { return JSON.stringify({ subject_id: buildSubjectId(), subject_attributes: { country: SUBJECT_COUNTRY }, flags: [FLAG_KEY], }); } export default function () { const response = http.post(`${API_URL}/decide`, buildPayload(), { headers: { "Content-Type": "application/json" }, tags: { endpoint: "decide" }, }); decideRequests.add(1); decideDuration.add(response.timings.duration); decideStatus200Rate.add(response.status === 200); let reason = ""; if (response.status === 200) { const body = response.json(); if (body && body.decisions && body.decisions.length > 0) { reason = String(body.decisions[0].reason || ""); } } decideAssignedRate.add(reason === "experiment_assigned"); check(response, { "status is 200": (r) => r.status === 200, "has one decision": (r) => { const body = r.json(); return ( body !== null && typeof body === "object" && Array.isArray(body.decisions) && body.decisions.length === 1 ); }, }); if (THINK_TIME_SECONDS > 0) { sleep(THINK_TIME_SECONDS); } }