From 6735fcccd06ec2e37229f121b72a9a391ce186e7 Mon Sep 17 00:00:00 2001 From: Ludek Novy <13610612+ludeknovy@users.noreply.github.com> Date: Wed, 5 Apr 2023 22:33:54 +0200 Subject: [PATCH] thresholds reworked (#316) --- .../item-detail/item-detail.component.html | 2 +- src/app/item-detail/item-detail.module.ts | 3 +- .../request-stats-compare.component.css | 7 +- .../request-stats-compare.component.html | 254 ++++++++++-------- .../request-stats-compare.component.spec.ts | 1 - .../request-stats-compare.component.ts | 67 +++-- .../threshold-failure.component.css | 0 .../threshold-failure.component.html | 12 + .../threshold-failure.component.spec.ts | 25 ++ .../threshold-failure.component.ts | 18 ++ .../thresholds-alert.component.css | 6 + .../thresholds-alert.component.html | 37 +-- src/app/items.service.model.ts | 90 ++++--- src/app/scenario.service.model.ts | 1 + .../scenario-settings.component.html | 11 +- .../scenario-settings.component.ts | 9 +- 16 files changed, 328 insertions(+), 215 deletions(-) create mode 100644 src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.css create mode 100644 src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.html create mode 100644 src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.spec.ts create mode 100644 src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.ts diff --git a/src/app/item-detail/item-detail.component.html b/src/app/item-detail/item-detail.component.html index 9f9633fd..d3cc547f 100644 --- a/src/app/item-detail/item-detail.component.html +++ b/src/app/item-detail/item-detail.component.html @@ -57,7 +57,7 @@
-
+
diff --git a/src/app/item-detail/item-detail.module.ts b/src/app/item-detail/item-detail.module.ts index 5ef25b21..ec757c93 100644 --- a/src/app/item-detail/item-detail.module.ts +++ b/src/app/item-detail/item-detail.module.ts @@ -28,6 +28,7 @@ import { ReloadCustomChartComponent } from "./analyze-charts/reload-custom-chart import { ExcelService } from "../_services/excel.service"; import { ChartIntervalComponent } from "./chart-interval/chart-interval.component"; import { ForbiddenComponent } from "../forbidden/forbidden.component"; +import { ThresholdFailureComponent } from "./request-stats/threshold-failure/threshold-failure.component"; const routes: Routes = [ { @@ -40,7 +41,7 @@ const routes: Routes = [ { declarations: [ItemDetailComponent, RequestStatsCompareComponent, ThresholdsAlertComponent, PerformanceAnalysisComponent, ZeroErrorToleranceWarningComponent, LabelChartComponent, AnalyzeChartsComponent, LabelHealthComponent, LabelTrendComponent, StatsCompareComponent, AddMetricComponent, ShareComponent, DeleteShareLinkComponent, - CreateNewShareLinkComponent, MonitoringStatsComponent, ReloadCustomChartComponent, ChartIntervalComponent, ForbiddenComponent ], + CreateNewShareLinkComponent, MonitoringStatsComponent, ReloadCustomChartComponent, ChartIntervalComponent, ForbiddenComponent, ThresholdFailureComponent ], imports: [ CommonModule, NgbModule, RouterModule.forRoot(routes), DataTableModule, SharedItemModule, SharedModule, HighchartsChartModule, ReactiveFormsModule, FormsModule, RoleModule, diff --git a/src/app/item-detail/request-stats/request-stats-compare.component.css b/src/app/item-detail/request-stats/request-stats-compare.component.css index 46f5737f..d93e4a0f 100644 --- a/src/app/item-detail/request-stats/request-stats-compare.component.css +++ b/src/app/item-detail/request-stats/request-stats-compare.component.css @@ -7,5 +7,10 @@ } .expand-button { - width: 30px; + width: 50px; +} + +.performance-regression-icon { + display: inline; + padding-left: 5px; } 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 f8bcdee3..2da90c5f 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 @@ -3,14 +3,14 @@
Request Statistics Comparing to test: {{comparedMetadata.id}} - with - {{comparedMetadata.maxVu}} VU + href="/project/{{params.projectName}}/scenario/{{params.scenarioName}}/item/{{comparedMetadata.id}}">{{comparedMetadata.id}} + with + {{comparedMetadata.maxVu}} VU @@ -18,13 +18,14 @@
Request Statistics
+ aria-label="Button group with nested dropdown"> + class="fas fa-bars"> + (keyup)='search($event.target.value)' [value]="externalSearchTerm">
@@ -185,18 +207,22 @@
  • Label with spaces: -

    If the searched label contains one or more spaces, you need to enclose it into double-quotes. Eg: "my search" +

    If the searched label contains one or more spaces, you need to enclose it into double-quotes. Eg: + "my search"

  • Not operator:

    If you want to exclude a particular label, the search needs to start with the not - keyword and followed by search term. Eg: not "my search term"

    + keyword and followed by search term. Eg: + not "my search term" +

  • OR operator:

    If you want to include only specific labels, you can join them by using the or keyword - and followed by search term. Eg: "my search term" or "another search term" + and followed by search term. Eg: + "my search term" or "another search term"

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 c62cef8b..8742fa48 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 @@ -11,7 +11,6 @@ import { StatsCompareComponent } from "../stats-compare/stats-compare.component" import { LabelHealthComponent } from "./label-health/label-health.component"; import { RequestStatsCompareComponent } from "./request-stats-compare.component"; -import { Metrics } from "../metrics"; describe("RequestStatsCompareComponent", () => { let component: RequestStatsCompareComponent; 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 e5190994..d36b8cf8 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 @@ -35,7 +35,7 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { comparedMetadata; defaultUnit = true; externalSearchTerm = ""; - collapsableSettings = {} + collapsableSettings = {}; constructor( private itemsService: ItemsApiService, @@ -48,7 +48,23 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { } ngOnInit() { - this.labelsData = this.itemData.statistics; + if (this.itemData?.thresholds?.passed === false) { + this.labelsData = this.itemData.statistics.map(labelData => { + const thresholdResult = this.itemData.thresholds.results.find(thresholdResult => + thresholdResult.label === labelData.label); + if (thresholdResult) { + return Object.assign(labelData, { + thresholdResult: { + passed: thresholdResult.passed, + result: thresholdResult.result + } + }); + } + return labelData; + }); + } else { + this.labelsData = this.itemData.statistics; + } this.analyzeChartService.currentData.subscribe(data => { if (data && data.label) { this.search(data.label); @@ -65,7 +81,7 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { this.comparedData = null; this.labelsData = this.itemData.statistics; this.defaultUnit = true; - this.comparisonChartService.resetPlot() + this.comparisonChartService.resetPlot(); } search(query: string) { @@ -123,8 +139,8 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { itemToCompare(data) { this.resetStatsData(); - this.comparisonChartService.setComparisonPlot(data.plot, data.extraPlotData) - this.comparisonChartService.setHistogramPlot(data.histogramPlotData) + this.comparisonChartService.setComparisonPlot(data.plot, data.extraPlotData); + this.comparisonChartService.setHistogramPlot(data.histogramPlotData); this.comparingData = data; this.comparedMetadata = { id: data.id, maxVu: data.maxVu }; if (data.maxVu !== this.itemData.overview.maxVu) { @@ -285,12 +301,14 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { } downloadAsXLXS() { - const { requestStats = { - samples: true, - avg: true, min: true, - max: true, p90: true, p95: true, - p99: true, throughput: true, network: true, - errorRate: true, standardDeviation: true } + const { + requestStats = { + samples: true, + avg: true, min: true, + max: true, p90: true, p95: true, + p99: true, throughput: true, network: true, + errorRate: true, standardDeviation: true + } } = this.itemData.userSettings; const dataToBeSaved = this.labelsData.map((label) => { return { @@ -319,18 +337,18 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { calculateApdex(satisfaction, toleration, samples) { if (satisfaction === null || toleration === null) { - return + return; } - const apdexValue = roundNumberTwoDecimals((satisfaction + (toleration * 0.5)) / samples) - return this.apdexScore(apdexValue) + 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)) + return roundNumberTwoDecimals(((item.apdex.satisfaction + (item.apdex.toleration * 0.5)) / item.samples)); } public sortByNetwork(item) { - return roundNumberTwoDecimals(item.bytesPerSecond + item.bytesSentPerSecond) + return roundNumberTwoDecimals(item.bytesPerSecond + item.bytesSentPerSecond); } private apdexScore(apdexValue: number): string { @@ -340,20 +358,21 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { { 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 + ]; + return score.find(sc => apdexValue >= sc.rangeFrom && apdexValue <= sc.rangeTo)?.name; } toggleSectionVisibility(event, index) { // eslint-disable-next-line no-prototype-builtins - if (!this.collapsableSettings.hasOwnProperty(index)) { - this.collapsableSettings[index] = true - } else { - this.collapsableSettings[index] = !this.collapsableSettings[index]; + if (!this.collapsableSettings.hasOwnProperty(index)) { + this.collapsableSettings[index] = true; + } else { + this.collapsableSettings[index] = !this.collapsableSettings[index]; - } + } } - identify(index, item){ + + identify(index, item) { return item.label; } } diff --git a/src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.css b/src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.css new file mode 100644 index 00000000..e69de29b diff --git a/src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.html b/src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.html new file mode 100644 index 00000000..3ed1b7cf --- /dev/null +++ b/src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.html @@ -0,0 +1,12 @@ +
+

+ + 90% Percentile performance regressed about {{thresholdResult?.result?.percentile?.diffValue - 100 | number: '1.0-2'}}% in comparison with baseline report. +

+

+ Throughput performance regressed about {{Math.abs(thresholdResult?.result?.throughput?.diffValue - 100) | number: '1.0-2'}}% in comparison with baseline report. +

+

+ Error Rate performance regressed about {{Math.abs(100 - thresholdResult?.result?.errorRate?.diffValue) | number: '1.0-2'}}% in comparison with baseline report. +

+
diff --git a/src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.spec.ts b/src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.spec.ts new file mode 100644 index 00000000..8263a4c2 --- /dev/null +++ b/src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { ThresholdFailureComponent } from "./threshold-failure.component"; + +describe("ThresholdFailureComponent", () => { + let component: ThresholdFailureComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ThresholdFailureComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ThresholdFailureComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.ts b/src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.ts new file mode 100644 index 00000000..9b9727d9 --- /dev/null +++ b/src/app/item-detail/request-stats/threshold-failure/threshold-failure.component.ts @@ -0,0 +1,18 @@ +import { Component, Input, OnInit } from "@angular/core"; +import { ThresholdResult } from "../../../items.service.model"; + +@Component({ + selector: "app-threshold-failure", + templateUrl: "./threshold-failure.component.html", + styleUrls: ["./threshold-failure.component.css"] +}) +export class ThresholdFailureComponent implements OnInit { + + @Input() thresholdResult: { passed: boolean, result: ThresholdResult }; + + ngOnInit(): void { + console.log(this.thresholdResult) + } + + protected readonly Math = Math; +} diff --git a/src/app/item-detail/thresholds-alert/thresholds-alert.component.css b/src/app/item-detail/thresholds-alert/thresholds-alert.component.css index 8bda044c..4922fe01 100644 --- a/src/app/item-detail/thresholds-alert/thresholds-alert.component.css +++ b/src/app/item-detail/thresholds-alert/thresholds-alert.component.css @@ -1,3 +1,9 @@ .card-body{ padding-top: 0px; } + +.perf-issue { + border-left: 4px solid #dc3545 !important; + padding-bottom: 10px; +} + diff --git a/src/app/item-detail/thresholds-alert/thresholds-alert.component.html b/src/app/item-detail/thresholds-alert/thresholds-alert.component.html index b2728ec6..ce163610 100644 --- a/src/app/item-detail/thresholds-alert/thresholds-alert.component.html +++ b/src/app/item-detail/thresholds-alert/thresholds-alert.component.html @@ -1,38 +1,9 @@ -

90 percentile decrease toleration: {{itemData.thresholds.thresholds.percentile}} % -

-

- Througput decrease toleration: {{itemData.thresholds.thresholds.throughput}}% -

-

- Error rate increase toleration: {{itemData.thresholds.thresholds.errorRate}}% -

-
Performance threshold failure Performance Regression Detected
-
-
90 percentile response time -
- 90 percentile response time is about {{Math.round(itemData.thresholds.result.percentile.diffValue - 100) }}% - slower than in the previous reports. -
-
-
Error rate -
- Error rate is about {{ - Math.round(100 - itemData.thresholds.result.errorRate.diffValue) }}% higher than the average. -
-
-
Throughput -
Throughput is about {{ - Math.round(100 - itemData.thresholds.result.throughput.diffValue ) }}% lower than in the previous - reports. -
-
-
+
diff --git a/src/app/items.service.model.ts b/src/app/items.service.model.ts index f0474001..8c6e6d8c 100644 --- a/src/app/items.service.model.ts +++ b/src/app/items.service.model.ts @@ -7,8 +7,8 @@ export interface ItemsListing { environment: string; status: string; zeroErrorToleranceEnabled: boolean; - thresholdPassed?: boolean - overview: ItemOverview + thresholdPassed?: boolean; + overview: ItemOverview; } @@ -41,10 +41,15 @@ export interface ItemDetail { extraPlotData: ItemExtraPlot[]; histogramPlotData?: { responseTimePerLabelDistribution?: ResponseTimePerLabelDistribution[] - } + }; statistics: ItemStatistics[]; thresholds?: { passed: boolean, + results: Array<{ + label: string + passed: string + result: ThresholdResult + }> diff: { errorRateDiff: number, percentileRateDiff: number, @@ -54,7 +59,7 @@ export interface ItemDetail { topMetricsSettings: TopMetricsSettings; userSettings: { requestStats: RequestStats - } + }; } interface TopMetricsSettings { @@ -116,8 +121,8 @@ export interface ItemDataPlot { } export interface ItemExtraPlot { - interval: string - data: ItemDataPlot + interval: string; + data: ItemDataPlot; } interface LabelSeries { @@ -141,7 +146,7 @@ export interface ItemStatistics { apdex: { satisfaction?: number toleration?: number - } + }; } interface ResponseMessageFailure { @@ -265,31 +270,31 @@ export interface ProjectsOverallStats { } export interface LabelTrend { - chartSeries: { - timePoints: string[]; - errorRate: number[]; - id: string; - p90: number[]; - p95: number[]; - p99: number[]; - throughput: number[]; - virtualUsers: number[]; - avgLatency: number[], - avgConnectionTime: number[], - avgResponseTime: number[], - name: string[], - }, - chartSettings: { - virtualUsers: boolean, - throughput: boolean, - avgLatency: boolean, - avgConnectionTime: boolean, - avgResponseTime: boolean, - p90: boolean, - p95: boolean, - p99: boolean, - errorRate: boolean, - } + chartSeries: { + timePoints: string[]; + errorRate: number[]; + id: string; + p90: number[]; + p95: number[]; + p99: number[]; + throughput: number[]; + virtualUsers: number[]; + avgLatency: number[], + avgConnectionTime: number[], + avgResponseTime: number[], + name: string[], + }, + chartSettings: { + virtualUsers: boolean, + throughput: boolean, + avgLatency: boolean, + avgConnectionTime: boolean, + avgResponseTime: boolean, + p90: boolean, + p95: boolean, + p99: boolean, + errorRate: boolean, + } } @@ -314,15 +319,30 @@ export interface UpsertItemChartSettings { } export interface ResponseTimePerLabelDistribution { - label: string - values: number[] + label: string; + values: number[]; } export interface ScenarioTrendsUserSettings { - aggregatedTrends: boolean + aggregatedTrends: boolean; labelMetrics: { errorRate: boolean percentile90: boolean throughput: boolean + }; +} + +export interface ThresholdResult { + errorRate: { + diffValue: number, + passed: boolean + } + percentile: { + diffValue: number, + passed: boolean + } + throughput: { + diffValue: number, + passed: boolean } } diff --git a/src/app/scenario.service.model.ts b/src/app/scenario.service.model.ts index e79c8598..5a2fd04d 100644 --- a/src/app/scenario.service.model.ts +++ b/src/app/scenario.service.model.ts @@ -13,6 +13,7 @@ export interface Scenario { userSettings: { requestStats?: RequestStats }; + baselineReport: string; } diff --git a/src/app/scenario/scenario-settings/scenario-settings.component.html b/src/app/scenario/scenario-settings/scenario-settings.component.html index 0845f4ce..ab9fbce8 100644 --- a/src/app/scenario/scenario-settings/scenario-settings.component.html +++ b/src/app/scenario/scenario-settings/scenario-settings.component.html @@ -132,18 +132,23 @@
Generate extra chart aggregations
- Scenario thresholds will raise an alert in a report detail in case the given metrics diverge from the previous reports' average by more than the specified threshold percentage.
+ Scenario thresholds will raise an alert in a report detail in case the given metrics diverge from the baseline report by more than the specified threshold percentage. +
+ +
+ You need to select a baseline report first. +
+ formControlName="thresholdEnabled" >
- +
diff --git a/src/app/scenario/scenario-settings/scenario-settings.component.ts b/src/app/scenario/scenario-settings/scenario-settings.component.ts index f3f20344..346a85a4 100644 --- a/src/app/scenario/scenario-settings/scenario-settings.component.ts +++ b/src/app/scenario/scenario-settings/scenario-settings.component.ts @@ -107,6 +107,7 @@ export class SettingsScenarioComponent implements OnInit { labelFilterOperators = ["includes", "match"]; labelFilters: FormArray; + hasBaselineReport = false constructor( @@ -120,6 +121,8 @@ export class SettingsScenarioComponent implements OnInit { ngOnInit(): void { this.route.params.subscribe(_ => this.params = _); this.scenarioApiService.getScenario(this.params.projectName, this.params.scenarioName).subscribe(_ => { + this.hasBaselineReport = !!_.baselineReport + console.log(this.hasBaselineReport) if (_.name) { this.createFormControls(_); this.createForm(); @@ -128,6 +131,8 @@ export class SettingsScenarioComponent implements OnInit { } + + this.apdexSettingsForm.valueChanges.subscribe(value => { const { satisfyingThreshold, toleratingThreshold } = value; if (satisfyingThreshold && toleratingThreshold) { @@ -171,7 +176,7 @@ export class SettingsScenarioComponent implements OnInit { Validators.max(100), Validators.required ]); - this.formControls.enabled = new FormControl(settings.thresholds.enabled, [ + this.formControls.enabled = new FormControl({ value: settings.thresholds.enabled, disabled: !this.hasBaselineReport }, [ Validators.required, ]); this.formControls.analysisEnabled = new FormControl(settings.analysisEnabled, [ @@ -307,7 +312,7 @@ export class SettingsScenarioComponent implements OnInit { generateShareToken, extraAggregations, thresholds: { - enabled: thresholdEnabled, + enabled: !!thresholdEnabled, errorRate: parseFloat(thresholdErrorRate), throughput: parseFloat(thresholdThroughput), percentile: parseFloat(thresholdPercentile)