/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import createDebug from 'debug'; const _debug = createDebug('plugin:apdex'); const METRICS = { satisfied: 'apdex.satisfied', tolerated: 'apdex.tolerated', frustrated: 'apdex.frustrated' }; class ApdexPlugin { // Untyped JS class - properties assigned dynamically [key: string]: any; constructor(script, _events) { this.script = script; if ( global.artillery && Number(global.artillery.version.slice(0, 1)) > 1 && typeof process.env.LOCAL_WORKER_ID !== 'undefined' ) { const t = script.config.apdex?.threshold ?? script.config.plugins?.apdex?.threshold ?? 500; if (!script.config.processor) { script.config.processor = {}; } // In the main thread config.processor may still be an unresolved // path (a string). Attaching functions to it was a silent no-op // under sloppy mode; ES modules are strict, so guard explicitly. // Workers load the processor into an object before plugins run. const canAttach = typeof script.config.processor === 'object'; script.scenarios.forEach((scenario) => { scenario.afterResponse = [].concat(scenario.afterResponse || []); scenario.afterResponse.push('apdexAfterResponse'); }); function apdexAfterResponse(_req, res, _userContext, events, done) { const total = res.timings.phases.total; events.emit('counter', METRICS.satisfied, 0); events.emit('counter', METRICS.tolerated, 0); events.emit('counter', METRICS.frustrated, 0); if (total <= t) { events.emit('counter', METRICS.satisfied, 1); } else if (total <= 4 * t) { events.emit('counter', METRICS.tolerated, 1); } else { events.emit('counter', METRICS.frustrated, 1); } return done(); } if (canAttach) { script.config.processor.apdexAfterResponse = apdexAfterResponse; } return; } global.artillery.ext({ ext: 'beforeExit', method: async (testInfo) => { const hasApdexConfig = this.script?.config?.apdex !== undefined || this.script?.config?.plugins?.apdex !== undefined; if (!hasApdexConfig) { return; } const s = testInfo.report.counters[METRICS.satisfied] || 0; const t = testInfo.report.counters[METRICS.tolerated] || 0; const f = testInfo.report.counters[METRICS.frustrated] || 0; const total = s + t + f; if (total > 0) { const apdexScore = (s + t / 2) / total; let ranking = ''; if (apdexScore >= 0.94) { ranking = 'excellent'; } else if (apdexScore >= 0.85) { ranking = 'good'; } else if (apdexScore >= 0.7) { ranking = 'fair'; } else if (apdexScore >= 0.49) { ranking = 'poor'; } else { ranking = 'unacceptable'; } global.artillery.apdexPlugin = { apdex: apdexScore, ranking }; console.log(`\nApdex score: ${apdexScore} (${ranking})`); } } }); } } export { ApdexPlugin as Plugin };