Skip to content

feat(scorecard): scorecard timeseries scalar aggregation - #4476

Open
dzemanov wants to merge 8 commits into
redhat-developer:mainfrom
dzemanov:scorecard/timeseries-aggregation-RHIDP-14402
Open

feat(scorecard): scorecard timeseries scalar aggregation#4476
dzemanov wants to merge 8 commits into
redhat-developer:mainfrom
dzemanov:scorecard/timeseries-aggregation-RHIDP-14402

Conversation

@dzemanov

@dzemanov dzemanov commented Aug 26, 2026

Copy link
Copy Markdown
Member

Hey, I just made a Pull Request!

  • Adds new endpoint GET /aggregations/:aggregationId/time-series
  • Endpoint GET /aggregations/:aggregationId/metadata now includes metadata.visualization, so frontend can determine what endpoint to call for aggregation data - use new endpoint GET /aggregations/:aggregationId/time-series if visualization = sparkline, use GET /aggregations/:aggregationId otherwise.
  • Removes MetricDefaultVisualizationType type in favor of ScorecardVisualizationType. The default visualization changes from 'value' to 'donut'.
  • When using default aggregation (no aggregation KPI block specified in app-config), the aggregation type is then:
    • average when the metric’s defaultVisualization is sparkline
    • statusGrouped otherwise
  • Updated .warning log to .info log when no aggregation KPI is configured and scorecard uses default aggregation, as it is not an invalid state, it's normal operation. It also reduces log noise.

GET /aggregations/:aggregationId/time-series:

  • Only scalar aggregation types (sum, average, max, min, count) are supported.

  • AggregationTypes statusGrouped and weightedStatusScore are not supported and return 400.

  • aggregationChartDisplayColor (in line with naming in weightedStatusScore aggregation result) comes from value of the last successful point classified against KPI thresholds.

  • No data UTC days are omitted from response.

  • Aggregation uses aggregationKPIs.<aggregationId>.options.thresholds if configured in app-config or by default DEFAULT_NUMBER_THRESHOLDS. This is in line with how scalar aggregation works. For me it would make sense to load default thresholds of a metric instead of using DEFAULT_NUMBER_THRESHOLDS to avoid redefining them in app-config and confusion with filter filtering by original threshold names, to be worked on in a separate ticket.

  • Supports default aggregation that uses :metricId, when no aggregation config is defined.

  • Example request

curl -X GET "{{url}}/api/scorecard/aggregations/avgDeploymentFrequency/time-series?from=2026-08-24T00:00:00.000Z&to=2026-08-24T23:59:59.999Z" \
  -H "Authorization: Bearer <token>"
  • Example response:
{
  "id": "avgDeploymentFrequency",
  "metricId": "dora.deploymentFrequency",
  "metadata": {
    "title": "Average Deployment Frequency",
    "description": "This KPI provides average weekly production deploys over a 30-day window per entity.",
    "type": "number",
    "unit": "/week",
    "history": true,
    "visualization": "sparkline",
    "aggregationType": "average"
  },
  "points": [
    {
      "value": 6.8,
      "successCount": 4,
      "errorCount": 3,
      "total": 7,
      "status": "success",
      "timestamp": "2026-08-24T00:00:00.000Z",
      "errors": [
        { "message": "GitHub API error", "count": 2 },
        { "message": "timeout", "count": 1 }
      ]
    }
  ],
  "thresholds": {
    "rules": [
      {
        "key": "elite",
        "expression": ">=7",
        "color": "success.main",
        "icon": "scorecardSuccessStatusIcon"
      },
      {
        "key": "medium",
        "expression": "1-7",
        "color": "warning.main",
        "icon": "scorecardWarningStatusIcon"
      },
      {
        "key": "error",
        "expression": "<1",
        "color": "error.main",
        "icon": "scorecardErrorStatusIcon"
      }
    ]
  },
  "aggregationChartDisplayColor": "warning.main"
}
  • Example KPI config
scorecard:
  aggregationKPIs:
    avgDeploymentFrequency:
      title: Average Deployment Frequency
      description: This KPI provides average weekly production deploys over a 30-day window per entity.
      type: average
      metricId: dora.deploymentFrequency
      options:
        thresholds:
          rules:
            - key: elite
              expression: '>=7'
              color: success.main
              icon: scorecardSuccessStatusIcon
            - key: medium
              expression: '1-7'
              color: warning.main
              icon: scorecardWarningStatusIcon
            - key: error
              expression: '<1'
              color: error.main
              icon: scorecardErrorStatusIcon
    avgEliteDeploymentFrequency:
      title: Average Elite Deployment Frequency
      description: This KPI provides average elite weekly production deploys over a 30-day window per entity.
      type: average
      metricId: dora.deploymentFrequency
      options:
        thresholds:
          rules:
            - key: elite
              expression: '>=7'
              color: success.main
              icon: scorecardSuccessStatusIcon
            - key: medium
              expression: '1-7'
              color: warning.main
              icon: scorecardWarningStatusIcon
            - key: error
              expression: '<1'
              color: error.main
              icon: scorecardErrorStatusIcon
      filter:
        status: elite

Fixes

Fixes https://redhat.atlassian.net/browse/RHIDP-14402

How to test

Requirements: jq, podman, sqlite3

1. Create test data

a) postgres

App-config

backend:
  database:
    client: pg
    connection:
      host: localhost
      port: 5432
      user: postgres
      password: backstage
podman run --name backstage-psql -e POSTGRES_PASSWORD=backstage -it -d -p 127.0.0.1:5432:5432 postgres

yarn start

podman exec -it backstage-psql psql -U postgres -d backstage_plugin_scorecard
INSERT INTO metric_values
  (metric_id, catalog_entity_ref, value, timestamp, error_message, status,
   entity_kind, entity_namespace, entity_owner)
VALUES
  -- 2026-08-23: all elite (filter elite == unfiltered)
  ('dora.deploymentFrequency', 'component:default/dora-scorecard', '8',  '2026-08-23 12:00:00+00', NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/all-scorecards', '9',  '2026-08-23 12:00:00+00', NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/github-scorecard-only', '10', '2026-08-23 12:00:00+00', NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/jira-scorecard-only', '11', '2026-08-23 12:00:00+00', NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/dependabot-scorecard-only', '12', '2026-08-23 12:00:00+00', NULL, 'elite',  'component', 'default', 'user:development/guest'),

-- ignored, different owner
  ('dora.deploymentFrequency', 'component:default/all-scorecards-service-different-owner', '50', '2026-08-23 12:00:00+00', NULL, 'elite',  'component', 'default', 'user:development/guest'),

  -- 2026-08-24 mixed: 4 successes + 2 errors; older 99 on dora-scorecard is not used, newer 10 is used
  ('dora.deploymentFrequency', 'component:default/dora-scorecard', '99',  '2026-08-24 08:00:00+00', NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/dora-scorecard', '10',  '2026-08-24 18:00:00+00', NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/github-scorecard-only', '14',  '2026-08-24 18:00:00+00', NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/jira-scorecard-only', '3',   '2026-08-24 18:00:00+00', NULL, 'medium', 'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/all-scorecards', '0.2', '2026-08-24 18:00:00+00', NULL, 'low',    'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/openssf-scorecard-only', NULL,  '2026-08-24 18:00:00+00', 'timeout',         NULL, 'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/dependabot-scorecard-only', NULL,  '2026-08-24 18:00:00+00', 'GitHub API error', NULL, 'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/sonarqube-scorecard-only', NULL,  '2026-08-24 18:00:00+00', 'GitHub API error', NULL, 'component', 'default', 'user:development/guest'),

  -- 2026-08-25: errors only
  ('dora.deploymentFrequency', 'component:default/dora-scorecard', NULL, '2026-08-25 12:00:00+00', 'timeout',         NULL, 'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/all-scorecards', NULL, '2026-08-25 12:00:00+00', 'GitHub API error', NULL, 'component', 'default', 'user:development/guest');
b) sqlite

App-config

backend:
  database:
    client: better-sqlite3
    connection:
      directory: './db'
yarn start

sqlite3 /Path/to/rhdh-plugins/workspaces/scorecard/packages/backend/db/scorecard.sqlite
INSERT INTO metric_values
  (metric_id, catalog_entity_ref, value, timestamp, error_message, status,
   entity_kind, entity_namespace, entity_owner)
VALUES
  -- 2026-08-23: all elite (filter elite == unfiltered)
  ('dora.deploymentFrequency', 'component:default/dora-scorecard', '8',  1787486400000, NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/all-scorecards', '9',  1787486400000, NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/github-scorecard-only', '10', 1787486400000, NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/jira-scorecard-only', '11', 1787486400000, NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/dependabot-scorecard-only', '12', 1787486400000, NULL, 'elite',  'component', 'default', 'user:development/guest'),

-- ignored, different owner
  ('dora.deploymentFrequency', 'component:default/all-scorecards-service-different-owner', '50', 1787486400000, NULL, 'elite',  'component', 'default', 'user:development/guest'),

  -- 2026-08-24 mixed: 4 successes + 2 errors; older 99 on dora-scorecard is not used, newer 10 is used
  ('dora.deploymentFrequency', 'component:default/dora-scorecard', '99',  1787558400000, NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/dora-scorecard', '10',  1787594400000, NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/github-scorecard-only', '14',  1787594400000, NULL, 'elite',  'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/jira-scorecard-only', '3',   1787594400000, NULL, 'medium', 'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/all-scorecards', '0.2', 1787594400000, NULL, 'low',    'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/openssf-scorecard-only', NULL,  1787594400000, 'timeout',         NULL, 'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/dependabot-scorecard-only', NULL,  1787594400000, 'GitHub API error', NULL, 'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/sonarqube-scorecard-only', NULL,  1787594400000, 'GitHub API error', NULL, 'component', 'default', 'user:development/guest'),

  -- 2026-08-25: errors only
  ('dora.deploymentFrequency', 'component:default/dora-scorecard', NULL, 1787659200000, 'timeout',         NULL, 'component', 'default', 'user:development/guest'),
  ('dora.deploymentFrequency', 'component:default/all-scorecards', NULL, 1787659200000, 'GitHub API error', NULL, 'component', 'default', 'user:development/guest');

To view data, you can use SQLite Viewer VS Code Extension. Or you can use any preferred DBMS.

2. Test scalar time-series aggregations

Results for points for different scalar operations:

Point timestamp Original values in aggregation Error entities Not in aggregation Aggregation result value successCount ErrorCount total
2026-08-23T00:00:00.000Z 8, 9, 10, 11, 12 - 'different owner entity with value 50' AVG 10, SUM 50, MIN 8, MAX 12, COUNT 5 5 0 5
2026-08-24T00:00:00.000Z 10, 14, 3, 0.2, 1x timeout, 2x GH API error 'older dora-scorecard result with value 100' AVG 6.8, SUM 27.2, MIN 0.2, MAX 14, COUNT 4 4 3 7
2026-08-25T00:00:00.000Z - 1x timeout, 2x GH API error - null 0 2 2

Results for points for different scalar operations with elite filter KPI:

Point timestamp Original values in aggregation Error entities Not in aggregation Aggregation result value successCount ErrorCount total
2026-08-23T00:00:00.000Z 8, 9, 10, 11, 12 - 'different owner entity with value 50' AVG 10, SUM 50, MIN 8, MAX 12, COUNT 5 5 0 5
2026-08-24T00:00:00.000Z 10, 14 1x timeout, 2x GH API error 'older dora-scorecard result with value 100', 3 and 0.2 values that are not 'elite' status AVG 12, SUM 24, MIN 10, MAX 14, COUNT 2 2 3 5
2026-08-25T00:00:00.000Z - 1x timeout, 2x GH API error - null 0 2 2
FROM=2026-08-22T00:00:00.000Z
TO=2026-08-25T23:59:59.000Z
BASE=http://localhost:7007
TOKEN=$(curl "$BASE/api/auth/guest/refresh" | jq -r '.backstageIdentity.token')

Custom KPI

curl -H "Authorization: Bearer $TOKEN" \
 "$BASE/api/scorecard/aggregations/avgDeploymentFrequency/time-series?from=$FROM&to=$TO" | jq

Custom filter KPI

curl -H "Authorization: Bearer $TOKEN" \
 "$BASE/api/scorecard/aggregations/avgEliteDeploymentFrequency/time-series?from=$FROM&to=$TO" | jq

Default KPI (metricId) - uses metric title and description

curl -H "Authorization: Bearer $TOKEN" \
 "$BASE/api/scorecard/aggregations/dora.deploymentFrequency/time-series?from=$FROM&to=$TO" | jq

empty points

curl -H "Authorization: Bearer $TOKEN" \
 "$BASE/api/scorecard/aggregations/countDeploymentFrequency/time-series?from=2026-08-01T00:00:00.000Z&to=2026-08-11T00:00:00.000Z" | jq

only errors

curl -H "Authorization: Bearer $TOKEN" \
 "$BASE/api/scorecard/aggregations/avgDeploymentFrequency/time-series?from=2026-08-25T00:00:00.000Z&to=$TO" | jq

You can test out different scalar aggregation types by updating app-config KPI name and type:

  1. avgDeploymentFrequency, type: average (default)
  2. sumDeploymentFrequency, type: sum
  3. countDeploymentFrequency, type: count
  4. minDeploymentFrequency, type: min
  5. maxDeploymentFrequency, type: max
    For elite filter:
  6. avgEliteDeploymentFrequency, type: average (default)
  7. sumEliteDeploymentFrequency, type: sum
  8. countEliteDeploymentFrequency, type: count
  9. minEliteDeploymentFrequency, type: min
  10. maxEliteDeploymentFrequency, type: max
    Note: you can update app-config.yaml without stopping running instance, make sure when using different custom KPI, to also update :aggregationId in URL from avgDeploymentFrequency or avgEliteDeploymentFrequency

Test cases covered by test data:

  • different owner entity is filtered out
  • aggregation of success values
  • unique errors messages are aggregated with correct count, sorted by their count
  • entity latest value is used
  • no data days are not included in result
  • filter works fine
  • aggregationChartDisplayColor is classified against latest successful value of metric in time range

3. Test invalid inputs

No provider registered

curl -H "Authorization: Bearer $TOKEN" \  
 "$BASE/api/scorecard/aggregations/invalid/time-series?from=$FROM&to=$TO" | jq 

Invalid from

curl -H "Authorization: Bearer $TOKEN" \
 "$BASE/api/scorecard/aggregations/avgDeploymentFrequency/time-series?from=2026invalid&to=$TO" | jq

Invalid to

curl -H "Authorization: Bearer $TOKEN" \
 "$BASE/api/scorecard/aggregations/avgDeploymentFrequency/time-series?from=$FROM&to=2026invalid" | jq

invalid range

curl -H "Authorization: Bearer $TOKEN" \
 "$BASE/api/scorecard/aggregations/avgDeploymentFrequency/time-series?from=2024-08-22T00:00:00.000Z&to=2026-08-22T00:00:00.000Z" | jq

invalid from>to

curl -H "Authorization: Bearer $TOKEN" \
 "$BASE/api/scorecard/aggregations/avgDeploymentFrequency/time-series?from=2024-08-22T00:00:00.000Z&to=2024-08-20T00:00:00.000Z" | jq

No permissions:

permission:
  enabled: true
curl -H "Authorization: Bearer $TOKEN" \
 "$BASE/api/scorecard/aggregations/avgDeploymentFrequency/time-series?from=$FROM&to=$TO" | jq

✔️ Checklist

  • A changeset describing the change and affected packages. (more info)
  • Added or Updated documentation
  • Tests for new functionality and regression tests for bug fixes
  • Screenshots attached (for UI changes)

@rhdh-qodo-merge

Copy link
Copy Markdown

PR Summary by Qodo

Add scalar aggregation time-series support

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds authenticated daily scalar aggregation histories across owned entities.
• Reports sparse UTC points, calculation errors, thresholds, and chart color.
• Unifies visualization types and selects aggregation defaults from metric visualization.
Diagram

graph TD
  Client["Scorecard client"] --> Route["Time-series route"] --> Auth["Access checks"] --> Service["Aggregation service"] --> Strategy["Scalar strategy"] --> Loader["Metric loader"] --> Database["Metric database"]
  Database --> Loader --> Strategy --> Service --> Route --> Client
Loading
High-Level Assessment

The chosen layered approach is appropriate: it extends the existing aggregation strategy abstraction, performs daily rollups in one database round-trip, and reuses existing authorization and metadata mapping. Loading raw samples for application-side aggregation was considered but would increase transfer volume and memory use, while generating empty calendar days would conflict with the intentionally sparse response contract.

Files changed (42) +3426 / -109

Enhancement (21) +744 / -71
getEntityMetrics.tsAccept donut visualization in entity metrics action +1/-1

Accept donut visualization in entity metrics action

• Replaces the obsolete value visualization enum with donut while retaining sparkline support.

workspaces/scorecard/plugins/scorecard-backend/src/actions/getEntityMetrics.ts

listMetrics.tsExpose donut visualization in metric listings +1/-1

Expose donut visualization in metric listings

• Updates the list action schema to return donut or sparkline visualization values.

workspaces/scorecard/plugins/scorecard-backend/src/actions/listMetrics.ts

DatabaseMetricValues.tsQuery scalar aggregates per UTC day +151/-15

Query scalar aggregates per UTC day

• Adds a single-round-trip CTE query that selects each entity's latest daily row and computes scalar values, success/error counts, and grouped error messages. Centralizes database-specific timestamp and numeric expressions.

workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts

types.tsDefine database time-series point types +14/-0

Define database time-series point types

• Introduces internal scalar daily point and grouped calculation-error models.

workspaces/scorecard/plugins/scorecard-backend/src/database/types.ts

buildScalarTimeSeriesPoints.tsBuild daily points from joined query rows +78/-0

Build daily points from joined query rows

• Consolidates joined error rows by UTC day, coerces driver numeric values, sorts errors, and removes empty points.

workspaces/scorecard/plugins/scorecard-backend/src/database/utils/buildScalarTimeSeriesPoints.ts

getAggregateExpression.tsSupport conditional scalar SQL aggregation +19/-5

Support conditional scalar SQL aggregation

• Allows aggregate expressions to include only rows matching a supplied SQL predicate, including conditional counts.

workspaces/scorecard/plugins/scorecard-backend/src/database/utils/getAggregateExpression.ts

validateTimeSeriesQueryParams.tsValidate aggregation time-series ranges +37/-18

Validate aggregation time-series ranges

• Extracts shared date-range validation and adds middleware for aggregation requests that do not require a metricId query parameter.

workspaces/scorecard/plugins/scorecard-backend/src/middlewares/validateTimeSeriesQueryParams.ts

AggregatedMetricLoader.tsLoad scalar aggregation history +28/-0

Load scalar aggregation history

• Adds a loader path that reads daily database aggregates and maps them into public time-series points.

workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregatedMetricLoader.ts

AggregationsService.tsDispatch time series and derive default aggregation type +38/-15

Dispatch time series and derive default aggregation type

• Adds time-series strategy dispatch with InputError handling for unsupported types. Defaults sparkline metrics to average, other metrics to statusGrouped, and logs fallback resolution at info level.

workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.ts

ScalarAggregationStrategy.tsProduce threshold-classified scalar histories +52/-1

Produce threshold-classified scalar histories

• Loads daily scalar points, selects KPI or default number thresholds, and derives chart color from the last successful point.

workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/ScalarAggregationStrategy.ts

types.tsExtend aggregation strategies with optional history +14/-2

Extend aggregation strategies with optional history

• Adds an optional daily time-series operation to the aggregation strategy interface.

workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/types.ts

types.tsDefine aggregation time-range options +5/-0

Define aggregation time-range options

• Extends aggregation inputs with required from and to dates for history requests.

workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/types.ts

mappers.tsMap scalar histories into API responses +45/-1

Map scalar histories into API responses

• Adds metric visualization to aggregation metadata and maps database daily points into sparse API responses with thresholds and chart color.

workspaces/scorecard/plugins/scorecard-backend/src/service/mappers.ts

router.tsExpose authenticated aggregation time-series endpoint +62/-1

Expose authenticated aggregation time-series endpoint

• Adds GET /aggregations/:aggregationId/time-series with range validation, user and entity authorization, metric permission filtering, and service dispatch.

workspaces/scorecard/plugins/scorecard-backend/src/service/router.ts

classifyNumberAgainstThresholds.tsClassify scalar values against KPI thresholds +42/-0

Classify scalar values against KPI thresholds

• Finds the first matching numeric threshold rule and enriches standard rules with presentation defaults.

workspaces/scorecard/plugins/scorecard-backend/src/utils/aggregation/classifyNumberAgainstThresholds.ts

withStandardThresholdDefaults.tsSupply standard threshold colors and icons +53/-0

Supply standard threshold colors and icons

• Fills missing presentation fields for success, warning, and error threshold rules without overriding explicit values.

workspaces/scorecard/plugins/scorecard-backend/src/utils/aggregation/withStandardThresholdDefaults.ts

formatUtcDate.tsFormat UTC days as midnight timestamps +7/-0

Format UTC days as midnight timestamps

• Adds a helper that converts YYYY-MM-DD values into start-of-day UTC ISO timestamps.

workspaces/scorecard/plugins/scorecard-backend/src/utils/formatUtcDate.ts

Metric.tsAdopt shared scorecard visualization type +4/-11

Adopt shared scorecard visualization type

• Replaces MetricDefaultVisualization with ScorecardVisualizationType across metric result models, removing the value option.

workspaces/scorecard/plugins/scorecard-common/src/types/Metric.ts

aggregation.tsDefine public scalar time-series contracts +65/-0

Define public scalar time-series contracts

• Adds visualization metadata plus public daily point, error, response, threshold, and chart color types.

workspaces/scorecard/plugins/scorecard-common/src/types/aggregation.ts

index.tsExport scorecard visualization types +1/-0

Export scorecard visualization types

• Re-exports the new shared scorecard type module from the common package.

workspaces/scorecard/plugins/scorecard-common/src/types/index.ts

scorecard.tsIntroduce shared visualization constants and type +27/-0

Introduce shared visualization constants and type

• Defines donut and sparkline visualization constants and the public ScorecardVisualizationType union.

workspaces/scorecard/plugins/scorecard-common/src/types/scorecard.ts

Tests (16) +2249 / -20
mockDatabaseMetricValues.tsMock scalar time-series database reads +3/-0

Mock scalar time-series database reads

• Extends the database fixture with the new scalar aggregation history method.

workspaces/scorecard/plugins/scorecard-backend/fixtures/mockDatabaseMetricValues.ts

listMetrics.test.tsUpdate list metrics visualization expectation +1/-1

Update list metrics visualization expectation

• Changes the action fixture to expect donut as the non-sparkline visualization.

workspaces/scorecard/plugins/scorecard-backend/src/actions/listMetrics.test.ts

DatabaseMetricValues.test.tsCover daily scalar aggregation database behavior +991/-1

Cover daily scalar aggregation database behavior

• Adds PostgreSQL and SQLite coverage for UTC bucketing, latest daily samples, all scalar functions, filters, errors, empty data, and numeric edge cases.

workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.test.ts

buildScalarTimeSeriesPoints.test.tsTest scalar query row consolidation +90/-0

Test scalar query row consolidation

• Verifies daily error grouping and omission of rows without successes or calculation failures.

workspaces/scorecard/plugins/scorecard-backend/src/database/utils/buildScalarTimeSeriesPoints.test.ts

getAggregateExpression.test.tsTest conditional scalar SQL expressions +15/-0

Test conditional scalar SQL expressions

• Covers row inclusion predicates for sum, average, maximum, minimum, and count expressions.

workspaces/scorecard/plugins/scorecard-backend/src/database/utils/getAggregateExpression.test.ts

validateQueryAndParams.test.tsTest aggregation date-range validation +81/-1

Test aggregation date-range validation

• Covers required dates, ordering, equal timestamps, and the inclusive 365-day range limit.

workspaces/scorecard/plugins/scorecard-backend/src/middlewares/validateQueryAndParams.test.ts

plugin.api.test.tsExercise time-series API end to end +199/-2

Exercise time-series API end to end

• Adds backend integration coverage for scalar results, empty ranges, unsupported types, authentication, missing aggregations, and owned-entity aggregation.

workspaces/scorecard/plugins/scorecard-backend/src/plugin.api.test.ts

CatalogMetricService.test.tsReformat metric history assertion +3/-6

Reformat metric history assertion

• Simplifies an existing database call assertion without changing behavior.

workspaces/scorecard/plugins/scorecard-backend/src/service/CatalogMetricService.test.ts

AggregatedMetricLoader.test.tsTest scalar time-series loading and mapping +89/-0

Test scalar time-series loading and mapping

• Verifies empty-entity short-circuiting, database arguments, status filter forwarding, and point mapping.

workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregatedMetricLoader.test.ts

AggregationsService.test.tsTest time-series dispatch and visualization defaults +107/-3

Test time-series dispatch and visualization defaults

• Covers scalar strategy dispatch, unsupported aggregation errors, sparkline-to-average fallback, status-grouped fallback, caching, and informational logging.

workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.test.ts

scalarAggregationStrategy.test.tsTest scalar history thresholds and chart color +193/-2

Test scalar history thresholds and chart color

• Covers custom and default thresholds, latest successful point classification, empty histories, invalid configs, and filter propagation.

workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/scalarAggregationStrategy.test.ts

mappers.test.tsTest visualization and time-series response mapping +161/-1

Test visualization and time-series response mapping

• Verifies metadata visualization, daily status and error mapping, UTC timestamps, thresholds, and chart color response fields.

workspaces/scorecard/plugins/scorecard-backend/src/service/mappers.test.ts

router.test.tsTest time-series routing, validation, and authorization +190/-2

Test time-series routing, validation, and authorization

• Covers invalid ranges, missing metrics, denied access, missing identity, unsupported types, and successful responses for every scalar aggregation type.

workspaces/scorecard/plugins/scorecard-backend/src/service/router.test.ts

classifyNumberAgainstThresholds.test.tsTest numeric threshold classification defaults +34/-0

Test numeric threshold classification defaults

• Verifies matching standard rules receive their default chart color and icon.

workspaces/scorecard/plugins/scorecard-backend/src/utils/aggregation/classifyNumberAgainstThresholds.test.ts

withStandardThresholdDefaults.test.tsTest standard threshold presentation defaults +83/-0

Test standard threshold presentation defaults

• Covers default colors and icons, explicit overrides, partial defaults, and unchanged custom rules.

workspaces/scorecard/plugins/scorecard-backend/src/utils/aggregation/withStandardThresholdDefaults.test.ts

formatUtcDate.test.tsTest UTC day to ISO conversion +9/-1

Test UTC day to ISO conversion

• Verifies a UTC calendar day is formatted as midnight ISO-8601.

workspaces/scorecard/plugins/scorecard-backend/src/utils/formatUtcDate.test.ts

Documentation (4) +391 / -18
aggregation-time-series.mdDeclare minor releases and visualization breaking change +10/-0

Declare minor releases and visualization breaking change

• Adds release notes for scalar aggregation time series and metadata visualization. Documents removal of the old visualization type and the new donut default.

workspaces/scorecard/.changeset/aggregation-time-series.md

README.mdDocument time-series aggregation API and defaults +125/-7

Document time-series aggregation API and defaults

• Explains visualization-based default aggregation selection, the new endpoint contract, permissions, parameters, and response example. Updates visualization examples from value to donut.

workspaces/scorecard/plugins/scorecard-backend/README.md

aggregation.mdExpand aggregation behavior and endpoint reference +211/-5

Expand aggregation behavior and endpoint reference

• Documents daily scalar computation, filtering, errors, sparse days, thresholds, chart color, permissions, and default aggregation behavior.

workspaces/scorecard/plugins/scorecard-backend/docs/aggregation.md

report.api.mdPublish time-series and visualization API surface +45/-6

Publish time-series and visualization API surface

• Updates the generated API report with scalar history response models, grouped errors, metadata visualization, and the shared donut/sparkline type.

workspaces/scorecard/plugins/scorecard-common/report.api.md

Other (1) +42 / -0
app-config.yamlAdd deployment-frequency aggregation examples +42/-0

Add deployment-frequency aggregation examples

• Configures average deployment-frequency KPIs with explicit thresholds, including a status-filtered variant.

workspaces/scorecard/app-config.yaml

@rhdh-qodo-merge

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. UTC grouping shifts days 🐞 Bug ≡ Correctness
Description
getUtcDayExpr converts the UTC-assumed, timezone-less PostgreSQL column to timestamptz and then
formats it in the session timezone, so non-UTC PostgreSQL sessions assign samples near midnight to
the preceding or following day. This returns incorrect daily points and can merge or split entity
samples across UTC days.
Code

workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts[95]

+      ? "TO_CHAR(timestamp AT TIME ZONE 'UTC', 'YYYY-MM-DD')"
Relevance

●●● Strong

This is a concrete timezone correctness bug in new UTC grouping logic; no close rejection precedent
was found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new expression explicitly applies AT TIME ZONE 'UTC' before formatting, while the schema
defines the column as a timezone-less dateTime; therefore rendering the resulting timezone-aware
value is sensitive to the PostgreSQL session timezone despite the method's UTC contract.

workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts[89-96]
workspaces/scorecard/plugins/scorecard-backend/migrations/20250915113801_init.js[30-37]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
PostgreSQL UTC-day grouping currently depends on the session timezone because `AT TIME ZONE 'UTC'` turns the timezone-less timestamp into a timezone-aware value before `TO_CHAR` formats it.

## Issue Context
The migration creates `timestamp` with `dateTime` and no timezone option, while the database code documents that this column is treated as UTC. Formatting the stored UTC wall-clock timestamp directly avoids a second session-timezone conversion.

## Fix Focus Areas
- workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts[89-96]
- workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.test.ts[1928-1930]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Breaking change marked minor 🐞 Bug ⚙ Maintainability
Description
The changeset requests minor releases even though this PR removes an exported type, removes the
accepted 'value' member, and explicitly labels those changes as breaking. Publishing this under a
minor version violates the stable packages' compatibility contract and can break consumers that
accept minor upgrades.
Code

workspaces/scorecard/.changeset/aggregation-time-series.md[R2-3]

+'@red-hat-developer-hub/backstage-plugin-scorecard-common': minor
+'@red-hat-developer-hub/backstage-plugin-scorecard-backend': minor
Relevance

● Weak

Recent PR #4379 rejected the same request: changing minor changesets to major for a breaking public
interface change.

PR-#4379

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The same changeset requests minor bumps and explicitly states that the visualization type removal
and loss of 'value' support are breaking; the packages are on stable major version 4, and the
changed public type/schema lines demonstrate the incompatibility.

workspaces/scorecard/.changeset/aggregation-time-series.md[1-10]
workspaces/scorecard/plugins/scorecard-common/src/types/Metric.ts[17-45]
workspaces/scorecard/plugins/scorecard-backend/src/actions/getEntityMetrics.ts[62-65]
workspaces/scorecard/plugins/scorecard-common/package.json[1-4]
workspaces/scorecard/plugins/scorecard-backend/package.json[1-4]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The release metadata marks explicitly breaking public API changes as minor releases.

## Issue Context
Both affected packages are currently version 4.2.0. The common package removes `MetricDefaultVisualizationType` and contracts the visualization union, while the backend output schemas stop accepting `value`.

## Fix Focus Areas
- workspaces/scorecard/.changeset/aggregation-time-series.md[1-4]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 11 rules
✅ Cross-repo context — repo relationships
  Explored: repo: redhat-developer/rhdh (sha: d814f4b0)

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@rhdh-qodo-merge rhdh-qodo-merge Bot added documentation Improvements or additions to documentation enhancement New feature or request Tests labels Aug 26, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 26, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 4:35 PM UTC · Completed 4:55 PM UTC

Commit: b9acf33 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $15.88

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review

Findings

High

  • [breaking-api] workspaces/scorecard/.changeset/aggregation-time-series.md:2 — The changeset bumps both scorecard-common and scorecard-backend as minor, but the PR contains breaking changes explicitly labeled BREAKING in the changeset body: (1) The publicly exported type MetricDefaultVisualization is removed — downstream consumers importing it will get a compile error. (2) The literal 'value' is replaced by 'donut' — code comparing === 'value' will silently stop matching, and backend Zod schemas now reject 'value' at runtime. (3) The default aggregation type for metric IDs without KPI config changes from always statusGrouped to average for sparkline metrics, altering the REST API response shape. Per semver, these require a major bump.
    Remediation: Change the changeset for scorecard-common from minor to major. Consider whether scorecard-backend also needs a major bump for the behavioral REST API change. Alternatively, re-export MetricDefaultVisualization as a deprecated alias and keep 'value' in the union to preserve backward compatibility under minor.

Medium

  • [stale-doc] workspaces/scorecard/plugins/scorecard-backend/docs/providers.md:72 — The example metric provider comment says // Optional. Omit / undefined => 'value', but the 'value' visualization option has been removed and replaced with 'donut'. This file is not modified in the PR.
    Remediation: Change to // Optional. Omit / undefined => 'donut'. Use 'sparkline' when a time-series UI is intended.

Low

  • [scope-creep] workspaces/scorecard/plugins/scorecard-common/src/types/Metric.ts:30 — The PR bundles breaking type/enum changes (removing MetricDefaultVisualizationType, replacing 'value' with 'donut', changing default aggregation logic) with the time-series feature. While these may be prerequisites for the feature, they are logically separable concerns that affect downstream consumers independently.

  • [stale-doc] workspaces/scorecard/plugins/scorecard/README.md:472 — The frontend plugin README states metric IDs without a KPI block use "default statusGrouped aggregation", but sparkline metrics now default to average. The backend docs were updated but this file was not.

  • [naming-convention] workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts:72 — The constructor parameter type was widened from Knex<any, any[]> to plain Knex during the refactoring to hoist isPostgres to a class field.

  • [test-inadequate] workspaces/scorecard/plugins/scorecard-backend/src/service/router.test.ts:1758 — The router integration test for GET /aggregations/:aggregationId/time-series does not cover the filter.status case. Filter forwarding is tested at the unit level but not end-to-end through the router.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run

Review

Findings

Critical

  • [breaking-type-removal] workspaces/scorecard/plugins/scorecard-common/src/types/Metric.ts — The exported public type MetricDefaultVisualization (union 'value' | 'sparkline') is removed from scorecard-common. Any downstream repository or plugin that imports MetricDefaultVisualization will fail to compile. The replacement type ScorecardVisualizationType uses 'donut' | 'sparkline' — a different union that drops the 'value' member and adds 'donut'. This is a breaking change to the public API surface of an npm package, yet the changeset marks it as minor.
    Remediation: Either (1) bump scorecard-common to a new major version since this removes an exported type and changes the value domain, or (2) keep MetricDefaultVisualization as a deprecated re-export alias of ScorecardVisualizationType for at least one minor release cycle to give consumers time to migrate.

High

  • [breaking-value-change] workspaces/scorecard/plugins/scorecard-common/src/types/scorecard.ts:17 — The 'value' literal that was previously valid for Metric.defaultVisualization is no longer part of the replacement union ScorecardVisualizationType ('donut' | 'sparkline'). Any existing metric provider that returns defaultVisualization: 'value' in its metric definition will fail runtime Zod validation on the backend (z.enum(['donut', 'sparkline']) in getEntityMetrics.ts and listMetrics.ts). Existing stored/cached data or configuration referencing 'value' will break.
    Remediation: Add a backward-compatible migration path: either (a) include 'value' in ScorecardVisualizationType as a deprecated alias that maps to 'donut' at the API layer, or (b) document the migration and bump to major.

  • [semver-mismatch] workspaces/scorecard/.changeset/aggregation-time-series.md:2 — The changeset marks @red-hat-developer-hub/backstage-plugin-scorecard-common as a minor release, but the change removes a public exported type (MetricDefaultVisualization), removes a valid union member ('value'), and replaces it with a differently-named type having different members. The changeset's own description says BREAKING while marking minor — a self-contradiction. Per semver and the project's own .fullsend/AGENTS.md instructions (major for breaking API changes, removed exports, changed interfaces), this requires a major bump.
    Remediation: Change the changeset to major for scorecard-common, or preserve backward compatibility to keep the bump at minor.

Medium

  • [breaking-behavioral-change] workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.ts:78 — The default aggregation type for metrics without a KPI config block changed from always statusGrouped to conditionally average (when defaultVisualization === 'sparkline'). Consumers relying on the implicit statusGrouped default for sparkline metrics will now receive a different aggregation shape (scalar vs status-grouped). The log level also changed from warn to info, so operators monitoring for warnings about missing KPI config will stop seeing alerts.
    Remediation: Document this behavioral change prominently in the changelog.

  • [stale-documentation] workspaces/scorecard/plugins/scorecard-backend/docs/providers.md:72 — An inline comment in the code sample states // Optional. Omit / undefined => 'value'. Use 'sparkline' when a time-series UI is intended. but this PR removes 'value' as a valid defaultVisualization option. The default when omitted is now 'donut', not 'value'. This file is not modified by the PR, leaving stale documentation that will mislead developers writing metric providers.
    Remediation: Update the comment to: // Optional. Omit / undefined => 'donut'. Use 'sparkline' when a time-series UI is intended.

Low

  • [scope-creep] workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.ts:75 — The default aggregation type change (sparkline → average instead of statusGrouped) is architecturally motivated by the time-series feature but represents a separate behavioral change. See also: [breaking-behavioral-change] finding at this location.

  • [scope-creep] workspaces/scorecard/plugins/scorecard-common/src/types/Metric.ts:42 — The MetricDefaultVisualizationType removal is architecturally linked to the new visualization field on AggregationMetadata but represents a distinct breaking change. See also: [breaking-type-removal] finding at this location.

  • [naming-convention] workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts:165 — The new method getLatestIdsPerUTCDaySubquery uses uppercase UTC, which is inconsistent with getUtcDayExpr() in the same class.

  • [code-organization] workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/ScalarAggregationStrategy.ts:109[...points].reverse().find(...) creates a full copy of the points array. points.findLast(...) would be more concise (max 365 elements, no real performance concern).

  • [new-api-surface] workspaces/scorecard/plugins/scorecard-common/src/types/aggregation.ts:79AggregationMetadata gains a new optional visualization field. Additive and backward-compatible.

  • [naming-coherence] workspaces/scorecard/plugins/scorecard-common/src/types/aggregation.ts:153AggregatedMetricTimeSeriesResponse uses 'Response' while the snapshot equivalent uses 'Result' (AggregatedMetricResult). Minor naming asymmetry.

  • [architecture-coherence] workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/types.ts:26aggregateTimeSeries? is optional on the AggregationStrategy interface. Deliberate design choice since not all strategies support time-series, with runtime check in AggregationsService.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (2)

Review

Findings

Critical

  • [semver-mismatch] workspaces/scorecard/.changeset/aggregation-time-series.md — The changeset marks @red-hat-developer-hub/backstage-plugin-scorecard-common as minor, but the changeset body itself declares BREAKING and the diff confirms removal of a public exported type (MetricDefaultVisualization) and removal of the 'value' union member. Per semver and the project's own policy in .fullsend/AGENTS.md ("major for breaking API changes: removed exports, changed interfaces"), this should be a major bump. Publishing as minor means consumers on ^current will auto-upgrade and break at compile time.
    Remediation: Change the changeset bump for scorecard-common from minor to major, or provide a backward-compatible re-export alias so no consumer breaks on upgrade.

  • [breaking-type-removal] workspaces/scorecard/plugins/scorecard-common/src/types/Metric.ts — The exported public type MetricDefaultVisualization = 'value' | 'sparkline' has been removed and replaced by ScorecardVisualizationType = 'donut' | 'sparkline' in a new file. This is a compile-time breaking change for any downstream consumer that imports MetricDefaultVisualization by name. Additionally, the literal 'value' is no longer a valid member of the replacement union.
    Remediation: Re-export MetricDefaultVisualization as a deprecated alias mapped to ScorecardVisualizationType for at least one release cycle, and document that 'value' should be replaced with 'donut'.

High

  • [breaking-default-change] workspaces/scorecard/plugins/scorecard-common/src/types/scorecard.ts — The default visualization value changed from 'value' to 'donut', and 'value' is no longer accepted. The Zod schemas in getEntityMetrics.ts and listMetrics.ts now validate z.enum(['donut', 'sparkline']) instead of z.enum(['value', 'sparkline']), meaning existing API clients sending 'value' will receive validation errors at runtime. See also: [breaking-type-removal] finding at Metric.ts.
    Remediation: Either treat 'value' as a deprecated-but-accepted alias (map to 'donut' at the API boundary) until consumers migrate, or ensure the semver bump is major and coordinate the migration.

Medium

  • [stale-documentation] workspaces/scorecard/plugins/scorecard-backend/docs/providers.md:72 — The inline code comment states Omit / undefined => 'value', but this PR removes the 'value' visualization option and replaces the default with 'donut'. The type was also renamed from MetricDefaultVisualization to ScorecardVisualizationType.
    Remediation: Update line 72 to: // Optional. Omit / undefined => 'donut'. Use 'sparkline' when a time-series UI is intended.

Low

  • [scope-creep] workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.ts:75 — The default aggregation type fallback changed from always statusGrouped to conditionally average for sparkline metrics, and the log level changed from .warn to .info. Both changes are documented in the changeset and are prerequisites for the time-series feature.

  • [edge-case] workspaces/scorecard/plugins/scorecard-backend/src/database/utils/buildScalarTimeSeriesPoints.ts:57Number(row.success_count) || 0 coercion correctly handles legitimate 0 but would silently mask NaN from corrupt DB data as 0, reducing observability of query issues.

  • [error-handling-gap] workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.ts:75 — When an aggregation ID has no KPI config, the fallback calls getMetric(aggregationId). If the metric is also not found, the resulting NotFoundError lacks context about the fallback attempt.

  • [naming-consistency] workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts:70 — Constructor parameter type changed from Knex<any, any[]> to Knex. Both are functionally equivalent since Knex defaults to <any, any[]>.

  • [naming-convention] workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts:132getLatestIdsPerUTCDaySubquery uses uppercase UTC while the SQL alias uses utc_day. Internally consistent within the PR.

  • [code-organization] workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/ScalarAggregationStrategy.ts:30ThresholdEvaluator uses constructor injection with default, while WeightedStatusScoreAggregationStrategy creates it inline. Minor inconsistency; the PR approach is better for testability.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (3)

Review

Findings

Medium

  • [breaking-api] workspaces/scorecard/plugins/scorecard-common/src/types/Metric.ts — The exported public type MetricDefaultVisualization = 'value' | 'sparkline' is removed and replaced by ScorecardVisualizationType = 'donut' | 'sparkline'. This is a breaking change: the type name changes and the literal 'value' is removed. The changeset correctly labels this as BREAKING but uses a minor version bump. The project has precedent for shipping BREAKING changes as minor bumps (e.g., v2.3.0), so this may be intentional — but downstream consumers relying on semver ranges like ^x.y.z could be affected.
    Remediation: Consider bumping to major, or document explicitly why a minor bump is acceptable for this breaking change.

  • [breaking-api] workspaces/scorecard/plugins/scorecard-common/src/types/scorecard.ts — The new ScorecardVisualizationTypes replaces 'value' with 'donut', and the default visualization for metrics now returns 'donut'. Existing API consumers reading metadata.defaultVisualization and comparing against 'value' will no longer match. The Zod schema was changed from z.enum(['value', 'sparkline']) to z.enum(['donut', 'sparkline']).
    Remediation: Coordinate the migration with downstream consumers. Consider supporting both values during a transition period, or bump to major version.

  • [breaking-api] workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.ts:78 — The default aggregation type for metrics without a KPI config block changed from always statusGrouped to conditionally average when defaultVisualization === 'sparkline'. This changes the shape of the GET /aggregations/:aggregationId response for existing consumers who rely on the fallback default.
    Remediation: Document this behavioral change prominently in the migration guide.

  • [api-contract] workspaces/scorecard/plugins/scorecard-backend/src/service/mappers.ts:90 — The snapshot endpoint's toAggregatedMetricResult sets id: metric.id (the backing metric id), while the new time-series endpoint's toScalarAggregatedMetricTimeSeriesResponse sets id: aggregationConfig.id (the KPI/aggregation id). When a KPI id differs from its metric id (e.g., KPI totalOpenPrs backed by metric github.openPRs), the two endpoints return different values in the top-level id field.
    Remediation: Align the id field semantics or document the intentional difference.

  • [stale-doc] workspaces/scorecard/plugins/scorecard-backend/docs/providers.md:72 — Stale default-visualization comment references removed option 'value'. The inline comment says Omit / undefined => 'value' but this PR removes 'value' from the ScorecardVisualizationType union and changes the default to 'donut'.
    Remediation: Update the comment to reference 'donut' instead of 'value'.

Low

  • [api-report-inconsistency] workspaces/scorecard/plugins/scorecard-common/report.api.mdCollectorMetadata type is removed from report.api.md but the source file collector.ts still exports it and src/types/index.ts still re-exports ./collector. This may be an API extractor tooling quirk; verify by regenerating the report.

  • [scope-creep] workspaces/scorecard/.changeset/aggregation-time-series.md:16 — The BREAKING change notice references MetricDefaultVisualizationType but the actual removed type is named MetricDefaultVisualization (without the trailing Type). Fix the changeset to reference the correct type name.

  • [edge-case] workspaces/scorecard/plugins/scorecard-backend/src/database/utils/buildScalarTimeSeriesPoints.ts:27Number(row.success_count) || 0 uses the falsy-or pattern. While correct for 0, it would coerce NaN to 0, silently hiding data integrity issues. Low risk since the database query always returns numeric aggregates.

Previous run (4)

Review

Findings

Critical

  • [breaking-type-removal] workspaces/scorecard/plugins/scorecard-common/src/types/Metric.ts — The exported type MetricDefaultVisualization ('value' | 'sparkline') has been deleted from scorecard-common@4.2.0. Any downstream consumer that imports this type will get a compile-time error. It is replaced by ScorecardVisualizationType ('donut' | 'sparkline') from the new scorecard.ts file, but there is no re-export alias to maintain backward compatibility.
    Remediation: Either keep MetricDefaultVisualization as a deprecated re-export alias (export type MetricDefaultVisualization = ScorecardVisualizationType;), or bump scorecard-common to a new major version.

  • [breaking-enum-value-change] workspaces/scorecard/plugins/scorecard-common/src/types/scorecard.ts — The defaultVisualization field previously accepted 'value' as a valid literal. The new ScorecardVisualizationType replaces 'value' with 'donut'. Any existing consumer code or persisted data using 'value' will no longer type-check, and runtime comparisons (=== 'value') will fail. The Zod schemas in getEntityMetrics.ts and listMetrics.ts also now reject 'value'.
    Remediation: Consider keeping 'value' as a deprecated alias in ScorecardVisualizationType for one release cycle, or bump to a major version.

  • [semver-mismatch] workspaces/scorecard/.changeset/aggregation-time-series.md — The changeset bumps scorecard-common and scorecard-backend as minor, but the body explicitly states BREAKING. The package is at v4.2.0 (post-1.0), so semver requires a major bump for backward-incompatible changes.
    Remediation: Change the changeset to major for scorecard-common, or convert the type removal into a non-breaking deprecation.

Medium

  • [breaking-api] workspaces/scorecard/plugins/scorecard-common/report.api.md — The CollectorMetadata type was removed from the API report, but the type still exists in collector.ts (lines 27–34) and is still exported from types/index.ts via export * from './collector'. This appears to be an api-extractor regeneration artifact rather than an intentional removal.
    Remediation: Regenerate the API report so CollectorMetadata reappears, or explicitly remove it from collector.ts if the removal is intentional.

  • [stale-doc] workspaces/scorecard/plugins/scorecard-backend/docs/aggregation.md — The GET /aggregations/:aggregationId section says "A warning is logged on the server" but the code was changed from logger.warn to logger.info. The new "Default aggregation" section in the same file correctly says "logs an info", creating an internal contradiction.
    Remediation: Change "A warning is logged" to "An info message is logged" in the GET /aggregations/:aggregationId section.

Low

  • [scope-creep] workspaces/scorecard/plugins/scorecard-common/src/types/Metric.ts — The breaking type change (MetricDefaultVisualization removal, 'value''donut') is bundled with the time-series feature. While the visualization type change supports the new default aggregation logic, these are separate concerns.

  • [scope-creep] workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.ts:78 — Default aggregation type for sparkline metrics changed from statusGrouped to average. This behavioral change is documented in the changeset but bundled with the time-series feature.

  • [getter-caching-pattern] workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts:80 — The new isPostgres getter re-evaluates (this.dbClient as any).client?.config?.client on every access. Computing once in the constructor as a private readonly field would be cleaner, though the performance impact is negligible.

  • [unused parameter] workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/ScalarAggregationStrategy.ts:83options.thresholds (metric-level thresholds) is accepted via AggregationTimeSeriesOptions but never read in aggregateTimeSeries(). The method uses aggregationConfig.options?.thresholds instead. This is a pre-existing pattern from the snapshot aggregate() method.

  • [jsdoc-style] workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts — Typo: "aggrefated" should be "aggregated" in JSDoc comment on readScalarAggregatedMetricTimeSeriesByEntityRefs.

  • [missing-doc] workspaces/scorecard/plugins/scorecard-backend/README.md — Missing space: `catalog.entity.read`permission should be `catalog.entity.read` permission.

  • [log-level-change] workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.ts:81 — Log level for missing KPI config changed from warn to info. The deprecated route handler still uses logger.warn for its deprecation notice — semantically different but a minor inconsistency in log-level conventions.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (5)

Review

Findings

Critical

  • [breaking-api] workspaces/scorecard/plugins/scorecard-common/src/types/Metric.ts — The exported MetricDefaultVisualizationType type (previously MetricDefaultVisualization = 'value' | 'sparkline') is completely removed from scorecard-common. Any downstream consumer that imports this type will get a compile-time error. The changeset itself explicitly labels this as BREAKING, yet the changeset declares only a minor bump for scorecard-common, not a major bump.
    Remediation: Either bump scorecard-common to a major version, or re-export MetricDefaultVisualization as a deprecated alias of ScorecardVisualizationType to preserve backward compatibility under a minor bump.

  • [breaking-api] workspaces/scorecard/plugins/scorecard-common/src/types/scorecard.ts — The 'value' visualization option has been removed and replaced by 'donut'. Any consumer that was setting defaultVisualization: 'value' on a Metric will now fail type-checking and potentially at runtime. Combined with the minor changeset bump, this is a silent breaking change for consumers on caret ranges.
    Remediation: If backward compatibility is required under a minor bump, add 'value' to ScorecardVisualizationTypes as a deprecated alias. Alternatively, bump to a major version.

Medium

  • [missing-version-bump] workspaces/scorecard/.changeset/aggregation-time-series.md — The changeset declares minor bumps for both scorecard-common and scorecard-backend, but the changeset body itself states BREAKING: "The MetricDefaultVisualizationType type has been removed." A self-declared breaking change with a minor bump is contradictory and violates the project's own changeset guidance ("major for breaking API changes — removed exports, changed interfaces, dropped support").
    Remediation: Either change the changeset to major for scorecard-common, or remove the BREAKING label and provide the deprecated alias.

  • [breaking-config] workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.ts:78 — When no KPI config block exists for an aggregationId, the default aggregation type changed from always statusGrouped to conditionally average (when metric.defaultVisualization === 'sparkline'). This changes runtime behavior for existing deployments that rely on the implicit default — an integration that previously received a statusGrouped response shape for a sparkline metric will now receive a scalar response shape without any config change.
    Remediation: Document this behavioral change prominently in the changeset. Consider logging when the fallback type differs from the pre-change default.

  • [stale-doc] workspaces/scorecard/plugins/scorecard-backend/docs/providers.md:72 — Comment says Omit / undefined => 'value' but the 'value' visualization type has been removed by this PR. The new default is 'donut' (via ScorecardVisualizationTypes.DONUT).
    Remediation: Change the comment from 'value' to 'donut'.

Low

  • [doc-style] workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts:442 — JSDoc comment has typo: aggrefated should be aggregated.

  • [doc-style] workspaces/scorecard/plugins/scorecard-backend/README.md:497 — Example JSON response includes a JavaScript-style comment (// from value of last successful point classified againts KPI thresholds) inside a JSON code block. JSON does not support comments, and againts is a typo for against.

  • [stale-doc] workspaces/scorecard/plugins/scorecard/README.md:472 — States that a metric id without a KPI row uses "default statusGrouped aggregation". This PR changes the default so that sparkline metrics now default to average instead. The statement is no longer unconditionally true.

  • [data-exposure] workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts:494error_message field from stored metric_values rows is returned verbatim in the time-series response errors array. If a metric provider inadvertently includes sensitive details (API keys, internal hostnames, stack traces) in error messages, those would be exposed. This is the same exposure model as existing endpoints and is not a regression introduced by this PR.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

dzemanov and others added 3 commits August 27, 2026 10:54
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
@dzemanov
dzemanov force-pushed the scorecard/timeseries-aggregation-RHIDP-14402 branch from b9acf33 to 6b9c6c1 Compare August 27, 2026 08:56
@rhdh-gh-app

rhdh-gh-app Bot commented Aug 27, 2026

Copy link
Copy Markdown

Important

This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior.

Changed Packages

Package Name Package Path Changeset Bump Current Version
@red-hat-developer-hub/backstage-plugin-scorecard-backend workspaces/scorecard/plugins/scorecard-backend minor v4.2.0
@red-hat-developer-hub/backstage-plugin-scorecard-common workspaces/scorecard/plugins/scorecard-common minor v4.2.0

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 27, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 8:58 AM UTC · Ended 9:16 AM UTC

Commit: 6b9c6c1 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 8:58 AM UTC · Completed 9:16 AM UTC

Commit: 6b9c6c1 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $11.75

@dzemanov

Copy link
Copy Markdown
Member Author

QODO: UTC grouping shifts days: getUtcDayExpr converts the UTC-assumed, timezone-less PostgreSQL column to timestamptz.

Scorecard uses timestamp with timezone, code.
From https://knexjs.org/guide/schema-builder.html#timestamp: 'By default PostgreSQL creates column with timezone (timestamptz type).'
You would need to pass .timestamp('timestamp', { useTz: false, precision: 0 }) for timezone-less.

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 27, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 11:19 AM UTC · Ended 11:56 AM UTC

Commit: 1be72f1 · View workflow run →

@fullsend-ai-review
fullsend-ai-review Bot dismissed stale reviews from themself August 27, 2026 11:56

Superseded by updated review

fullsend-ai-review[bot]

This comment was marked as outdated.

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the requires-manual-review Review requires human judgment label Aug 27, 2026
@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:19 AM UTC · Completed 11:56 AM UTC

Commit: 1be72f1 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $13.78

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 27, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 12:16 PM UTC · Ended 12:31 PM UTC

Commit: 69e09fb · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot removed the requires-manual-review Review requires human judgment label Aug 27, 2026
@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:16 PM UTC · Completed 12:31 PM UTC

Commit: 69e09fb · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $12.45

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 27, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 12:50 PM UTC · Ended 12:56 PM UTC

Commit: fe54344 · View workflow run →

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 27, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 12:57 PM UTC · Ended 1:36 PM UTC

Commit: 79ad178 · View workflow run →

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.60479% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.66%. Comparing base (e0d0d5f) to head (efc855b).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4476      +/-   ##
==========================================
+ Coverage   61.61%   61.66%   +0.04%     
==========================================
  Files        2565     2568       +3     
  Lines      102788   102927     +139     
  Branches    28851    28893      +42     
==========================================
+ Hits        63333    63468     +135     
- Misses      37577    37581       +4     
  Partials     1878     1878              
Flag Coverage Δ *Carryforward flag
adoption-insights 84.55% <ø> (ø) Carriedforward from 79ad178
ai-integrations 69.09% <ø> (ø) Carriedforward from 79ad178
app-defaults 48.37% <ø> (ø) Carriedforward from 79ad178
augment 46.67% <ø> (ø) Carriedforward from 79ad178
boost 79.84% <ø> (ø) Carriedforward from 79ad178
bulk-import 72.79% <ø> (ø) Carriedforward from 79ad178
cost-management 13.55% <ø> (ø) Carriedforward from 79ad178
dcm 72.09% <ø> (ø) Carriedforward from 79ad178
e2e-adoption-insights 60.00% <ø> (ø) Carriedforward from 79ad178
e2e-extensions 62.13% <ø> (ø) Carriedforward from 79ad178
e2e-global-header 49.45% <ø> (ø) Carriedforward from 79ad178
e2e-homepage 61.11% <ø> (ø) Carriedforward from 79ad178
e2e-intelligent-assistant 46.65% <ø> (ø) Carriedforward from 79ad178
e2e-orchestrator 49.52% <ø> (ø) Carriedforward from 79ad178
e2e-orchestrator-plugin 49.51% <ø> (ø) Carriedforward from 79ad178
e2e-quickstart 55.21% <ø> (ø) Carriedforward from 79ad178
e2e-scorecard 50.21% <ø> (ø) Carriedforward from 79ad178
e2e-theme 16.36% <ø> (ø) Carriedforward from 79ad178
extensions 56.59% <ø> (ø) Carriedforward from 79ad178
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 79ad178
global-header 66.50% <ø> (ø) Carriedforward from 79ad178
homepage 48.39% <ø> (ø) Carriedforward from 79ad178
install-dynamic-plugins 58.57% <ø> (ø) Carriedforward from 79ad178
intelligent-assistant 75.35% <ø> (ø) Carriedforward from 79ad178
konflux 91.98% <ø> (ø) Carriedforward from 79ad178
lightspeed 69.02% <ø> (ø) Carriedforward from 79ad178
mcp-integrations 84.14% <ø> (ø) Carriedforward from 79ad178
orchestrator 70.87% <ø> (ø) Carriedforward from 79ad178
quickstart 63.74% <ø> (ø) Carriedforward from 79ad178
sandbox 79.56% <ø> (ø) Carriedforward from 79ad178
scorecard 87.67% <97.60%> (+0.26%) ⬆️
theme 88.91% <ø> (ø) Carriedforward from 79ad178
translations 5.12% <ø> (ø) Carriedforward from 79ad178
x2a 77.22% <ø> (ø) Carriedforward from 79ad178

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e0d0d5f...efc855b. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

fullsend-ai-review[bot]

This comment was marked as outdated.

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:57 PM UTC · Completed 1:36 PM UTC

Commit: 79ad178 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $14.48

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 27, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 1:45 PM UTC · Ended 2:05 PM UTC

Commit: efc855b · View workflow run →

@sonarqubecloud

Copy link
Copy Markdown

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: The following review comments could not be posted on the diff (GitHub returned 422) and are included here instead:

  • workspaces/scorecard/plugins/scorecard-common/src/types/Metric.ts (file-level): Line 30 · [low] scope-creep

The PR bundles breaking type/enum changes (removing MetricDefaultVisualizationType, replacing 'value' with 'donut', changing default aggregation logic) with the time-series feature. While these may be prerequisites for the feature, they are logically separable concerns that affect downstream consumers independently.

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

@@ -0,0 +1,10 @@
---
'@red-hat-developer-hub/backstage-plugin-scorecard-common': minor

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] breaking-api

The changeset bumps both scorecard-common and scorecard-backend as minor, but the PR contains breaking changes explicitly labeled BREAKING in the changeset body: (1) The publicly exported type MetricDefaultVisualization is removed. (2) The literal 'value' is replaced by 'donut' -- code comparing === 'value' will silently stop matching, and backend Zod schemas now reject 'value' at runtime. (3) The default aggregation type for metric IDs without KPI config changes from always statusGrouped to average for sparkline metrics, altering the REST API response shape. Per semver, these require a major bump.

Suggested fix: Change the changeset for scorecard-common from minor to major. Consider whether scorecard-backend also needs a major bump for the behavioral REST API change. Alternatively, re-export MetricDefaultVisualization as a deprecated alias and keep 'value' in the union to preserve backward compatibility under minor.

@@ -65,6 +70,14 @@ type ScalarAggregationRowResult = {

export class DatabaseMetricValues {
private readonly tableName = 'metric_values';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] naming-convention

The constructor parameter type was widened from Knex<any, any[]> to plain Knex during the refactoring to hoist isPostgres to a class field.

'github.openPRs',
)}?from=2024-02-01T00:00:00.000Z&to=2024-01-01T00:00:00.000Z`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] test-inadequate

The router integration test for GET /aggregations/:aggregationId/time-series does not cover the filter.status case. Filter forwarding is tested at the unit level but not end-to-end through the router.

@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:45 PM UTC · Completed 2:04 PM UTC

Commit: efc855b · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $12.27

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request Tests workspace/scorecard

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant