diff --git a/frontend/Makefile b/frontend/Makefile index da041899c..6d9aa71af 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -66,7 +66,7 @@ stop: ################################################## load-test-local: # Load test the local environment at localhost:3000 - artillery run artillery-load-test.yml + artillery run -e local artillery-load-test.yml load-test-dev: # Load test the dev environment in aws artillery run -e dev artillery-load-test.yml diff --git a/frontend/artillery-load-test.yml b/frontend/artillery-load-test.yml index 2d2d57f1f..d1c83e9cb 100644 --- a/frontend/artillery-load-test.yml +++ b/frontend/artillery-load-test.yml @@ -1,5 +1,5 @@ config: - target: http://localhost:3000 + target: "http://127.0.0.1:3000" tls: rejectUnauthorized: false http: @@ -18,6 +18,20 @@ config: maxVusers: 1000 name: Spike phase environments: + local: + target: http://localhost:3000 + phases: + - duration: 2 + arrivalRate: 5 + name: Warm up phase + - duration: 1 + arrivalRate: 1 + maxVusers: 1 + name: Ramp up load + - duration: 1 + arrivalRate: 1 + maxVusers: 1 + name: Spike phase prod: target: https://simpler.grants.gov staging: @@ -30,24 +44,34 @@ config: ensure: {} apdex: {} metrics-by-endpoint: {} - apdex: - threshold: 100 + processor: "./tests/artillery/processor.ts" + +before: + flow: + - function: loadData + scenarios: - - name: root + - name: Opportunity Pages + beforeScenario: getOppId flow: - get: - url: "/" - expect: - - statusCode: 200 - - name: health + url: "/opportunity/{{ id }}" + - log: "GET /opportunity/{{ id }}" + - name: 404 Pages + beforeScenario: get404 flow: - get: - url: "/health" - expect: - - statusCode: 200 - - name: hello + url: "/{{ route }}" + - log: "GET 404 page /{{ route }}" + - name: Static Pages + beforeScenario: getStatic flow: - get: - url: "/api/hello" - expect: - - statusCode: 200 + url: "/{{ route }}" + - log: "GET static page /{{ route }}" + - name: Searches + beforeScenario: getSearchQuery + flow: + - get: + url: "/search?{{ query }}" + - log: "GET /search?{{ query }}" diff --git a/frontend/tests/artillery/params.json b/frontend/tests/artillery/params.json new file mode 100644 index 000000000..ecb30e494 --- /dev/null +++ b/frontend/tests/artillery/params.json @@ -0,0 +1,168 @@ +{ + "ids": { + "local": [ + 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35 + ], + "dev": [], + "prod": [] + }, + "queries": [ + "test", + "grants", + "education", + "transportation", + "trauma", + "veterans" + ], + "status": ["posted", "forecasted", "closed", "archived", "none"], + "agencies": [ + "ARPAH", + "USAID", + "USAID-AFG", + "USAID", + "USAID-ARM", + "USAID-AZE", + "USAID-BAN", + "USAID-BEN", + "AC", + "DC", + "USDA", + "USDA-AMS", + "USDA-FNS1", + "DOC", + "DOC-DOCNOAAERA", + "DOC-EDA", + "DOC-NIST", + "DOD", + "DOD-AMC-ACCAPGN", + "DOD-AMC-ACCAPGD", + "DOD-AFRL-AFRLDET8", + "DOD-AFRL", + "DOD-USAFA", + "DOD-AFOSR", + "DOD-DARPA-BTO", + "ED", + "DOE", + "DOE-ARPAE", + "DOE-GFO", + "DOE-01", + "PAMS", + "PAMS-SC", + "HHS", + "HHS-ACF-FYSB", + "HHS-ACF", + "HHS-ACF-CB", + "DHS", + "DHS-DHS", + "DHS-OPO", + "DHS-USCG", + "HUD", + "USDOJ", + "USDOJ-OJP-BJA", + "USDOJ-OJP-COPS", + "DOL", + "DOL-ETA-ILAB", + "DOL-ETA-CEO", + "DOS", + "DOS-NEA-AC", + "DOS-DRL", + "DOS-ECA", + "DOI", + "DOI-BIA", + "DOI-BLM", + "DOI-BOR", + "USDOT", + "USDOT-ORP", + "USDOT-DO-SIPPRA", + "USDOT-GCR", + "DOT", + "DOT-DOT X-50", + "DOT-RITA", + "DOT-FAA-FAA ARG", + "DOT-FRA", + "DOT-FHWA", + "DOT-FTA", + "DOT-FAA-FAA COE-AJFE", + "DOT-FAA-FAA COE-FAA JAMS", + "DOT-FAA-FAA COE-TTHP", + "DOT-MA", + "DOT-NHTSA", + "VA", + "VA-CSHF", + "VA-HPGPDP", + "VA-LSV", + "VA-NVSP", + "VA-NCAC", + "VA-OMHSP", + "VA-SSVF", + "VA-NCA", + "VA-VLGP", + "EPA", + "IMLS", + "MCC", + "NASA", + "NASA-HQ", + "NASA-JSC", + "NASA-SFC", + "NASA", + "NARA", + "NEA", + "NEH", + "NSF", + "SSA" + ], + "eligibility": [ + "state_governments", + "county_governments", + "city_or_township_governments", + "special_district_governments", + "independent_school_districts", + "public_and_state_institutions_of_higher_education", + "private_institutions_of_higher_education", + "federally_recognized_native_american_tribal_governments", + "other_native_american_tribal_organizations", + "public_and_indian_housing_authorities", + "nonprofits_non_higher_education_with_501c3", + "nonprofits_non_higher_education_without_501c3", + "for_profit_organizations_other_than_small_businesses", + "small_businesses", + "other", + "unrestricted" + ], + "funding": [ + "cooperative_agreement", + "grant", + "procurement_contract", + "other" + ], + "category": [ + "recovery_act", + "agriculture", + "arts", + "business_and_commerce", + "community_development", + "consumer_protection", + "disaster_prevention_and_relief", + "education", + "employment_labor_and_training", + "energy", + "environment", + "food_and_nutrition", + "health", + "housing", + "humanities", + "information_and_statistics", + "infrastructure_investment_and_jobs_act", + "income_security_and_social_services", + "law_justice_and_legal_services", + "natural_resources", + "opportunity_zone_benefits", + "regional_development", + "science_technology_and_other_research_and_development", + "transportation", + "affordable_care_act", + "other" + ], + "pages": ["", "process", "research", "subscribe", "subscribe/confirmation"] +} diff --git a/frontend/tests/artillery/processor.ts b/frontend/tests/artillery/processor.ts new file mode 100644 index 000000000..a6a2681c2 --- /dev/null +++ b/frontend/tests/artillery/processor.ts @@ -0,0 +1,129 @@ +import { readFile } from "fs/promises"; +import { random } from "lodash"; + +type dataType = { + ids: { + [key: string]: Array; + }; + queries: Array; + pages: Array; + status: Array; + agencies: Array; + funding: Array; + eligibility: Array; + category: Array; +}; +type globalVars = { + $environment?: string; +}; + +type returnVars = { + id: number; + query: string; + route: string; + pages: string; +}; + +// eslint-disable-next-line @typescript-eslint/require-await +async function getOppId(context: { vars: dataType & returnVars & globalVars }) { + const env = context.vars.$environment as string; + context.vars.id = + context.vars.ids[env][random(context.vars.ids[env].length - 1)]; +} + +// eslint-disable-next-line @typescript-eslint/require-await +async function get404(context: { vars: returnVars }) { + const num = random(10); + // ~50% of 404s are opp pages. + if (num % 2 !== 0) { + context.vars.route = `opportunity/${num}`; + } else { + context.vars.route = randomString(num); + } +} + +// eslint-disable-next-line @typescript-eslint/require-await +async function getStatic(context: { vars: returnVars }) { + context.vars.route = + context.vars.pages[random(context.vars.pages.length - 1)]; +} + +// eslint-disable-next-line @typescript-eslint/require-await +async function getSearchQuery(context: { vars: returnVars & dataType }) { + const { queries, status, agencies, eligibility, category } = context.vars; + const queryParam = `query=${queries[random(queries.length - 1)]}`; + const statusParam = `status=${status[random(status.length - 1)]}`; + const agencyParam = `agency=${agencies[random(agencies.length - 1)]}`; + const categoryParam = `category=${category[random(category.length - 1)]}`; + const eligibilityParam = `eligibility=${eligibility[random(eligibility.length - 1)]}`; + const pagerParam = `page=${random(5)}`; + // Most search params only include the queries, but smaller percent include + // filters. This allows configuring that percent for composing the query + const weights = [ + { + percent: 50, + params: [queryParam, statusParam, pagerParam], + }, + { + percent: 20, + params: [queryParam, statusParam, agencyParam], + }, + { + percent: 20, + params: [queryParam, statusParam, agencyParam, categoryParam], + }, + { + percent: 10, + params: [ + queryParam, + statusParam, + agencyParam, + categoryParam, + eligibilityParam, + ], + }, + ]; + // Weight of percents out of 100 + const hundred = random(100); + let total = 0; + const selected = weights.find((item) => { + total += item.percent; + if (hundred <= total) { + return true; + } + return false; + }); + context.vars.query = selected?.params.join("&") as string; +} + +async function loadData(context: { vars: dataType & globalVars }) { + // Dev and stage have the same data. + const env = + context.vars.$environment === "stage" ? "dev" : context.vars.$environment; + const envs = new Set(["local", "dev", "stage", "prod"]); + if (!env || !envs.has(env)) { + throw new Error(`env ${env ?? ""} does not exist in env list`); + } + const path = "./tests/artillery/params.json"; + const file = await readFile(path, "utf8"); + context.vars = JSON.parse(file) as dataType; +} + +function randomString(length: number) { + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = " "; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(random(charactersLength))); + } + return result; +} + +module.exports = { + loadData, + getOppId, + get404, + getStatic, + getSearchQuery, +};