diff --git a/src/app/item-detail/request-stats/request-stats-compare.component.html b/src/app/item-detail/request-stats/request-stats-compare.component.html index e8269516..61fbdb0b 100644 --- a/src/app/item-detail/request-stats/request-stats-compare.component.html +++ b/src/app/item-detail/request-stats/request-stats-compare.component.html @@ -86,6 +86,9 @@
Request Statistics error rate [%] + + apdex + @@ -125,6 +128,11 @@
Request Statistics {{_.errorRate | number: '1.0-2'}} + + + {{ calculateApdex(_.apdex?.satisfaction,_.apdex?.toleration, _.samples)}} + + diff --git a/src/app/item-detail/request-stats/request-stats-compare.component.spec.ts b/src/app/item-detail/request-stats/request-stats-compare.component.spec.ts index a46eee64..5608b522 100644 --- a/src/app/item-detail/request-stats/request-stats-compare.component.spec.ts +++ b/src/app/item-detail/request-stats/request-stats-compare.component.spec.ts @@ -77,6 +77,7 @@ describe("RequestStatsCompareComponent", () => { errorRate: true, min: true, p95: true, + apdex: true, } }, baseId: "", @@ -96,6 +97,10 @@ describe("RequestStatsCompareComponent", () => { responseMessageFailures: [ { responseMessage: "error", count: 1 } ], + apdex: { + toleration: 100, + satisfaction: 400, + } }, { avgResponseTime: 10, bytes: 758, @@ -111,6 +116,10 @@ describe("RequestStatsCompareComponent", () => { responseMessageFailures: [ { responseMessage: "error", count: 1 } ], + apdex: { + toleration: 20, + satisfaction: 4, + } }, { avgResponseTime: 10, @@ -127,6 +136,10 @@ describe("RequestStatsCompareComponent", () => { responseMessageFailures: [ { responseMessage: "error", count: 1 } ], + apdex: { + toleration: 10, + satisfaction: 40, + } }, ] }; fixture.detectChanges(); diff --git a/src/app/item-detail/request-stats/request-stats-compare.component.ts b/src/app/item-detail/request-stats/request-stats-compare.component.ts index 30b61d96..42afe366 100644 --- a/src/app/item-detail/request-stats/request-stats-compare.component.ts +++ b/src/app/item-detail/request-stats/request-stats-compare.component.ts @@ -33,6 +33,7 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { comparedMetadata; defaultUnit = true; externalSearchTerm = ""; + displayApdexColumn = false; constructor( private itemsService: ItemsApiService, @@ -51,6 +52,7 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { this.externalSearchTerm = data.label; } }); + this.displayApdexColumn = !!this.itemData.statistics.find(stats => stats.apdex.satisfaction && stats.apdex.toleration) } ngOnDestroy() { @@ -307,4 +309,28 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { } return value; } + + calculateApdex(satisfaction, toleration, samples) { + const apdexValue = roundNumberTwoDecimals((satisfaction + (toleration * 0.5)) / samples) + return this.apdexScore(apdexValue) + } + + public sortByApdex(item) { + return roundNumberTwoDecimals(((item.apdex.satisfaction + ( item.apdex.toleration * 0.5)) / item.samples)) + } + + private apdexScore(apdexValue: number): string { + const score = [ + { rangeFrom: 0.94, rangeTo: 1, name: "Excellent" }, + { rangeTo: 0.93, rangeFrom: 0.85, name: "Good" }, + { rangeFrom: 0.7, rangeTo: 0.83, name: "Fair", }, + { rangeFrom: 0.5, rangeTo: 0.69, name: "Poor", }, + { rangeFrom: 0, rangeTo: 0.49, name: "Unacceptable" } + ] + return score.find(sc => apdexValue >= sc.rangeFrom && apdexValue <= sc.rangeTo)?.name + } + + shouldApdexColumnBeDisplayed(): boolean { + return this.displayApdexColumn + } } diff --git a/src/app/items.service.model.ts b/src/app/items.service.model.ts index 9c2530d2..a1c58612 100644 --- a/src/app/items.service.model.ts +++ b/src/app/items.service.model.ts @@ -135,6 +135,10 @@ export interface ItemStatistics { samples: number; throughput: number; responseMessageFailures?: ResponseMessageFailure[]; + apdex: { + satisfaction?: number + toleration?: number + } } interface ResponseMessageFailure { diff --git a/src/app/scenario.service.model.ts b/src/app/scenario.service.model.ts index 015fe8de..7f61f007 100644 --- a/src/app/scenario.service.model.ts +++ b/src/app/scenario.service.model.ts @@ -27,4 +27,5 @@ export interface RequestStats { p99: boolean samples: boolean throughput: boolean + apdex: boolean } diff --git a/src/app/scenario/scenario-settings/scenario-settings.component.html b/src/app/scenario/scenario-settings/scenario-settings.component.html index eb840b4e..549d7a6d 100644 --- a/src/app/scenario/scenario-settings/scenario-settings.component.html +++ b/src/app/scenario/scenario-settings/scenario-settings.component.html @@ -354,6 +354,11 @@
Generate extra chart aggregations
formControlName="errorRate"> +
+ + +
@@ -366,6 +371,48 @@
Generate extra chart aggregations
+
  • + APDEX configuration + +
    +
    + + + +
    +
    + + +
    +
    + +
    + + + +
    +
    + + + +
    + +
    +
    + +
    +
  • +
    diff --git a/src/app/scenario/scenario-settings/scenario-settings.component.ts b/src/app/scenario/scenario-settings/scenario-settings.component.ts index 3c140959..f6cca4e9 100644 --- a/src/app/scenario/scenario-settings/scenario-settings.component.ts +++ b/src/app/scenario/scenario-settings/scenario-settings.component.ts @@ -17,12 +17,13 @@ export class SettingsScenarioComponent implements OnInit { @Output() scenarioNameChangeEvent = new EventEmitter(); - active = "general" + active = "general"; scenarioSettingsForm: FormGroup; labelTrendChartSettingsForm: FormGroup; requestStatsSettingsForm: FormGroup; - labelFiltersForm: FormGroup + labelFiltersForm: FormGroup; + apdexSettingsForm: FormGroup; formControls = { scenarioName: null, @@ -49,8 +50,8 @@ export class SettingsScenarioComponent implements OnInit { avgConnectionTime: null, avgLatency: null, errorRate: null, - } - labelFilterControls = {} + }; + labelFilterControls = {}; requestStatsCormControls = { samples: null, avg: null, @@ -62,7 +63,13 @@ export class SettingsScenarioComponent implements OnInit { throughput: null, network: null, errorRate: null, - } + apdex: null, + }; + apdexFormControls = { + apdexEnabled: null, + satisfyingThreshold: null, + toleratingThreshold: null + }; params; @@ -94,12 +101,11 @@ export class SettingsScenarioComponent implements OnInit { } ]; - labelFiltersData = [{ term: "my label", operator: "includes" }, { term: "test", operator: "match" }] - + labelFiltersData = [{ term: "my label", operator: "includes" }, { term: "test", operator: "match" }]; - labelFilterOperators = ["includes", "match"] - labelFilters: FormArray + labelFilterOperators = ["includes", "match"]; + labelFilters: FormArray; constructor( @@ -107,7 +113,8 @@ export class SettingsScenarioComponent implements OnInit { private modalService: NgbModal, private scenarioApiService: ScenarioApiService, private notification: NotificationMessage, - ) { } + ) { + } ngOnInit(): void { this.route.params.subscribe(_ => this.params = _); @@ -116,10 +123,30 @@ export class SettingsScenarioComponent implements OnInit { this.createFormControls(_); this.createForm(); this.labelFilters = new FormArray( - _.labelFilterSettings.map(filter=>new FormArray([new FormControl(filter.labelTerm,Validators.required), new FormControl(filter.operator, Validators.required)]))) + _.labelFilterSettings.map(filter => new FormArray([new FormControl(filter.labelTerm, Validators.required), new FormControl(filter.operator, Validators.required)]))); } + + + this.apdexSettingsForm.valueChanges.subscribe(value => { + const { satisfyingThreshold, toleratingThreshold } = value; + if (satisfyingThreshold && toleratingThreshold) { + if (Number(satisfyingThreshold) > Number(toleratingThreshold)) { + this.apdexSettingsForm.get("satisfyingThreshold").setErrors({ + "minIsGreaterThanMax": true + }); + this.apdexSettingsForm.get("toleratingThreshold").setErrors({ + "maxIsLowerThenMin": true, + }); + } else { + this.apdexSettingsForm.get("satisfyingThreshold").updateValueAndValidity({ onlySelf: true }); + this.apdexSettingsForm.get("toleratingThreshold").updateValueAndValidity({ onlySelf: true }); + } + } + }); + }); + } createFormControls(settings) { @@ -163,7 +190,7 @@ export class SettingsScenarioComponent implements OnInit { ]); this.formControls.extraAggregations = new FormControl(settings.extraAggregations, [ Validators.required - ]) + ]); this.labelTrendChartControls.virtualUsers = new FormControl(settings.labelTrendChartSettings?.virtualUsers, []); this.labelTrendChartControls.throughput = new FormControl(settings.labelTrendChartSettings?.throughput, []); @@ -175,18 +202,25 @@ export class SettingsScenarioComponent implements OnInit { this.labelTrendChartControls.p99 = new FormControl(settings.labelTrendChartSettings?.p99, []); this.labelTrendChartControls.errorRate = new FormControl(settings.labelTrendChartSettings?.errorRate, []); - this.requestStatsCormControls.samples = new FormControl(settings.userSettings.requestStats.samples, [Validators.required]) - this.requestStatsCormControls.avg = new FormControl(settings.userSettings.requestStats.avg, [Validators.required]) - this.requestStatsCormControls.min = new FormControl(settings.userSettings.requestStats.min, [Validators.required]) - this.requestStatsCormControls.max = new FormControl(settings.userSettings.requestStats.max, [Validators.required]) - this.requestStatsCormControls.p90 = new FormControl(settings.userSettings.requestStats.p90, [Validators.required]) - this.requestStatsCormControls.p95 = new FormControl(settings.userSettings.requestStats.p95, [Validators.required]) - this.requestStatsCormControls.p99 = new FormControl(settings.userSettings.requestStats.p99, [Validators.required]) - this.requestStatsCormControls.throughput = new FormControl(settings.userSettings.requestStats.throughput, [Validators.required]) - this.requestStatsCormControls.network = new FormControl(settings.userSettings.requestStats.network, [Validators.required]) - this.requestStatsCormControls.errorRate = new FormControl(settings.userSettings.requestStats.errorRate, [Validators.required]) - - + this.requestStatsCormControls.samples = new FormControl(settings.userSettings.requestStats.samples, [Validators.required]); + this.requestStatsCormControls.avg = new FormControl(settings.userSettings.requestStats.avg, [Validators.required]); + this.requestStatsCormControls.min = new FormControl(settings.userSettings.requestStats.min, [Validators.required]); + this.requestStatsCormControls.max = new FormControl(settings.userSettings.requestStats.max, [Validators.required]); + this.requestStatsCormControls.p90 = new FormControl(settings.userSettings.requestStats.p90, [Validators.required]); + this.requestStatsCormControls.p95 = new FormControl(settings.userSettings.requestStats.p95, [Validators.required]); + this.requestStatsCormControls.p99 = new FormControl(settings.userSettings.requestStats.p99, [Validators.required]); + this.requestStatsCormControls.throughput = new FormControl(settings.userSettings.requestStats.throughput, [Validators.required]); + this.requestStatsCormControls.network = new FormControl(settings.userSettings.requestStats.network, [Validators.required]); + this.requestStatsCormControls.errorRate = new FormControl(settings.userSettings.requestStats.errorRate, [Validators.required]); + this.requestStatsCormControls.apdex = new FormControl(settings.userSettings.requestStats.apdex || false, [Validators.required]); + + this.apdexFormControls.satisfyingThreshold = new FormControl(settings?.apdexSettings?.satisfyingThreshold, [ + Validators.min(0), + Validators.max(999999)]); + this.apdexFormControls.toleratingThreshold = new FormControl(settings?.apdexSettings?.toleratingThreshold, [ + Validators.min(0), + Validators.max(999999)]); + this.apdexFormControls.apdexEnabled = new FormControl(settings.apdexSettings?.enabled, []); } createForm() { @@ -214,7 +248,7 @@ export class SettingsScenarioComponent implements OnInit { p95: this.labelTrendChartControls.p95, p99: this.labelTrendChartControls.p99, errorRate: this.labelTrendChartControls.errorRate, - }) + }); this.requestStatsSettingsForm = new FormGroup({ samples: this.requestStatsCormControls.samples, @@ -226,11 +260,15 @@ export class SettingsScenarioComponent implements OnInit { p99: this.requestStatsCormControls.p99, throughput: this.requestStatsCormControls.throughput, network: this.requestStatsCormControls.network, - errorRate: this.requestStatsCormControls.errorRate - }) - this.labelFiltersForm = new FormGroup({ - - }) + errorRate: this.requestStatsCormControls.errorRate, + apdex: this.requestStatsCormControls.apdex, + }); + this.labelFiltersForm = new FormGroup({}); + this.apdexSettingsForm = new FormGroup({ + toleratingThreshold: this.apdexFormControls.toleratingThreshold, + satisfyingThreshold: this.apdexFormControls.satisfyingThreshold, + apdexEnabled: this.apdexFormControls.apdexEnabled, + }); } @@ -239,13 +277,23 @@ export class SettingsScenarioComponent implements OnInit { } onSubmit() { - if (this.scenarioSettingsForm.valid && this.labelFilters.valid && this.labelTrendChartSettingsForm.valid && this.requestStatsSettingsForm) { + if (this.scenarioSettingsForm.valid && this.labelFilters.valid && this.labelTrendChartSettingsForm.valid + && this.requestStatsSettingsForm && this.apdexSettingsForm.valid) { const { - scenarioName, analysisEnabled, - thresholdEnabled, thresholdErrorRate, - thresholdPercentile, thresholdThroughput, deleteSamples, zeroErrorToleranceEnabled, keepTestRunsPeriod, generateShareToken, extraAggregations + scenarioName, + analysisEnabled, + thresholdEnabled, + thresholdErrorRate, + thresholdPercentile, + thresholdThroughput, + deleteSamples, + zeroErrorToleranceEnabled, + keepTestRunsPeriod, + generateShareToken, + extraAggregations } = this.scenarioSettingsForm.value; + const { apdexEnabled, satisfyingThreshold, toleratingThreshold } = this.apdexSettingsForm.value const { projectName, scenarioName: currentScenarioName } = this.params; const body = { scenarioName, @@ -265,6 +313,11 @@ export class SettingsScenarioComponent implements OnInit { labelTrendChartSettings: this.labelTrendChartSettingsForm.value, userSettings: { requestStats: this.requestStatsSettingsForm.value, + }, + apdexSettings: { + enabled: apdexEnabled, + satisfyingThreshold, + toleratingThreshold } }; @@ -283,13 +336,13 @@ export class SettingsScenarioComponent implements OnInit { } addFieldValue(operator) { - const element = document.getElementById("term") - this.labelFilters.push(new FormArray([new FormControl(element.value,Validators.required), new FormControl(operator, Validators.required)])) - element.value = "" + const element = document.getElementById("term"); + this.labelFilters.push(new FormArray([new FormControl(element.value, Validators.required), new FormControl(operator, Validators.required)])); + element.value = ""; } deleteFieldValue(index) { - this.labelFilters.removeAt(index) + this.labelFilters.removeAt(index); } private scenarioNameChanged(name): boolean {