Skip to content

feat(metrics): add metrics plugin poc - #145

Open
rsoaresd wants to merge 2 commits into
konflux-ci:mainfrom
rsoaresd:metrics_api_poc
Open

feat(metrics): add metrics plugin poc#145
rsoaresd wants to merge 2 commits into
konflux-ci:mainfrom
rsoaresd:metrics_api_poc

Conversation

@rsoaresd

@rsoaresd rsoaresd commented Aug 12, 2026

Copy link
Copy Markdown

Summary

Currently, we are facing a few challenges while maintaining n8n workflows:

  • no unit tests
  • changes are done often directly into production
  • no robust version control (in the most cases of our nodes backup, we are just saving ‘Process Data’ node of the workflow, which is a gap since there are other important nodes of a workflow that we should track too (like for example changing ‘Get Prs’)
  • duplication of functions like calculateMedian, isAutogenerated and isBotReview
  • duplication of nodes like ‘Get Prs’, ‘Normalize Input’, etc..

All together can lead to bugs, since we are not keeping track of all the changes and we are not testing them. As an alternative, we could create a Standalone HTTP API, written in Go,  that queries DevLake’s MySQL database and exposes pre-computed metrics.

Issue ticket number and link

DPROD-1388

@rsoaresd
rsoaresd requested a review from a team as a code owner August 12, 2026 10:14
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 12, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:15 AM UTC · Completed 10:36 AM UTC

Commit: 9103755 · View workflow run →

@codecov-commenter

codecov-commenter commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 18.61%. Comparing base (8fc7c76) to head (7da4bac).

❗ There is a different number of reports uploaded between BASE (8fc7c76) and HEAD (7da4bac). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (8fc7c76) HEAD (7da4bac)
unit-tests-python 2 1
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main     #145       +/-   ##
===========================================
- Coverage   40.75%   18.61%   -22.14%     
===========================================
  Files         147      147               
  Lines       10152    10152               
===========================================
- Hits         4137     1890     -2247     
- Misses       5911     8236     +2325     
+ Partials      104       26       -78     
Flag Coverage Δ
e2e-go 9.45% <ø> (ø)
unit-tests-go ?
unit-tests-python 55.49% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 28 files with indirect coverage changes


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 8fc7c76...7da4bac. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

High

  • [Missing-Authentication] backend/plugins/metrics/cmd/main.go:48 — The entire metrics API is exposed without any authentication or authorization. All endpoints are publicly accessible to anyone who can reach the network port. The API reads from DevLake's MySQL database containing repository metadata, PR titles, descriptions, author names, PR URLs, and review comments.
    Remediation: Add an authentication mechanism (API key via header, OAuth2 bearer token, or mutual TLS). At minimum, require a shared secret via environment variable validated in middleware before any handler executes.

Medium

  • [logic-error] backend/plugins/metrics/api/routes/pr/handlers/key_metrics.go:41 — The key-metrics handler silently continues and returns partial data when the outliers query fails. The error is logged but execution continues to WriteJSON, producing a response with a valid-looking outlier_prs count from stats but an empty drill-down list.
    Remediation: Either return an error for the outlier query failure or set the Warning field on the response.

  • [edge-case] backend/plugins/metrics/transform/pr/zscore.go:63 — When computeBaseline returns no valid lookback PRs, it returns muLog=0, sigmaLog=1e-10. The z-score calculation produces extremely large values, classifying ALL current-period PRs as "Slow" when there is no lookback data.
    Remediation: When computeBaseline returns fallback values, return an empty/warning response or categorize all PRs as "Average."

  • [Dockerfile-correctness] backend/plugins/metrics/Dockerfile:1 — Dockerfile uses golang:1.26-alpine as the builder image. Go 1.26 does not exist. This will cause a build failure when the image is pulled.
    Remediation: Change the base image to match the Go version in go.mod (e.g., golang:1.21-alpine or golang:1.22-alpine).

  • [unbounded-request-body] backend/plugins/metrics/api/params.go:47DecodeQueryParams calls json.NewDecoder(r.Body).Decode without limiting body size. An attacker can send a multi-gigabyte POST body to exhaust server memory (DoS).
    Remediation: Wrap r.Body with http.MaxBytesReader before decoding.

  • [no-input-validation] backend/plugins/metrics/api/routes/pr/handlers/utils.go:29toParams passes user-supplied values to SQL query parameters without validation. While parameterized queries prevent SQL injection, no validation on BlueprintID format, timestamp ranges, or array lengths means excessively large arrays generate long IN clauses causing query parsing overhead.
    Remediation: Validate BlueprintID matches ^[0-9,]+$, From > 0, To > From, cap array lengths.

  • [missing-build-command] docs/local-dev.md:60 — The "Building (from backend/)" section does not mention the new make build-metrics-api target. A developer following this doc would not discover how to build the standalone binary.
    Remediation: Add make build-metrics-api # Build standalone metrics API binary to the build commands list.

Low

  • [missing-input-validation] backend/plugins/metrics/api/params.goDecodeQueryParams and toParams perform no validation on required fields. Empty blueprintid/repos produce empty result sets silently rather than returning 400.
  • [edge-case] backend/plugins/metrics/api/routes/pr/handlers/key_metrics.go:33 — Previous-period calculation creates a 1-second gap between periods; the From>0 guard prevents the degenerate case.
  • [population-vs-sample-variance] backend/plugins/metrics/transform/pr/zscore.go:150computeBaseline uses population variance (divides by N) instead of sample variance (N-1). Impact is negligible for a typical 90-day lookback but matters for small N.
  • [inconsistent-response-typing] backend/plugins/metrics/transform/pr/stages.goBuildStages returns map[string]interface{} instead of typed model structs, inconsistent with other transforms.
  • [inconsistent-response-typing] backend/plugins/metrics/transform/pr/scatter.goBuildScatter returns map[string]interface{} instead of model.LineData, inconsistent with BuildCycleTime.
  • [race-condition] backend/plugins/metrics/api/server.go:38WriteJSON writes Content-Type header then encodes; if Encode fails partway, client receives partial 200 response. Standard Go HTTP limitation.
  • [test-inadequate] backend/plugins/metrics/transform/pr/zscore_test.go:48TestBuildZScore_Categories uses single-PR lookback (degenerate sigma=1e-10), asserting total==2 without verifying category assignments.
  • [CORS-fail-open] backend/plugins/metrics/api/middleware.go:27 — CORS origin is configurable via env var; if set to *, any browser origin can make requests. Default (https://devtools.pages.redhat.com) is safe.
  • [information-disclosure] backend/plugins/metrics/api/server.go:40 — Healthz returns raw MySQL error messages to clients, potentially leaking connection details.
  • [credential-handling] backend/plugins/metrics/cmd/main.go:57 — Default MySQL credentials could cause partial misconfiguration. MYSQL_PASS is required, mitigating the worst case.
  • [no-rate-limiting] backend/plugins/metrics/cmd/main.go:34 — No rate limiting. The 10-connection DB pool provides implicit limiting, but explicit rate limiting is recommended before production use.
  • [missing-adr] docs/adr/ — Architectural decision to introduce a standalone HTTP binary is documented in plugin docs/README.md but not as a formal ADR per CLAUDE.md section 3.2.
  • [scope-alignment] PR title says "poc" but implementation is production-grade (7 endpoints, comprehensive tests, distroless Dockerfile, health checks, detailed docs).
  • [naming-coherence] backend/plugins/metrics/ — Plugin name "metrics" is generic; existing owned plugins have domain-specific names. Consider "prmetrics" for clarity.
  • [scope-creep] backend/plugins/metrics/docs/README.md — Plugin docs placed under plugin directory rather than central docs/, though this is consistent with owned-plugin patterns.
  • [package-naming] backend/plugins/metrics/api/routes/pr/handlers/ — Package declares package pr instead of package handlers (directory name), forcing callers to use prhandlers alias.
  • [type-usage] backend/plugins/metrics/model/response.go — Uses interface{} instead of any (Go 1.18+ alias) throughout.
  • [missing-doc-comment] backend/plugins/metrics/api/routes/pr/register.go — Exported Register function lacks a doc comment per CLAUDE.md Go profile section 18.
  • [naming-consistency] backend/plugins/metrics/testsupport/time.goParseTime silently discards errors from time.Parse, returning zero-value on failure.
  • [stale-enumeration] docs/upstream-diffs.md:6 — Owned plugins list does not include the new metrics plugin.
  • [missing-build-command] CLAUDE.md — DevLake Local Development section does not mention make build-metrics-api.
  • [missing-service-url] docs/local-dev.md:16 — Service URLs table does not include the metrics-api service (port 8181).

Labels: New metrics plugin feature with a high-severity missing-authentication finding requiring human security judgment.


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 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.


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

  • backend/plugins/metrics/cmd/main.go:48: [high] Missing-Authentication

The entire metrics API is exposed without any authentication or authorization. All endpoints are publicly accessible. The API reads from DevLake MySQL containing repository metadata, PR titles, descriptions, author names, PR URLs, and review comments.

Suggested fix: Add an authentication mechanism (API key via header, OAuth2, or mutual TLS). At minimum, require a shared secret via environment variable validated in middleware before any handler executes.

  • backend/plugins/metrics/api/routes/pr/handlers/key_metrics.go:41: [medium] logic-error

The key-metrics handler silently continues and returns partial data when the outliers query fails. The error is logged but execution continues to WriteJSON, producing a response with a valid-looking outlier_prs count from stats but an empty drill-down list.

Suggested fix: Either return an error for the outlier query failure or set the Warning field on the response.

  • backend/plugins/metrics/transform/pr/zscore.go:63: [medium] edge-case

When computeBaseline returns no valid lookback PRs, it returns muLog=0, sigmaLog=1e-10. The z-score calculation produces extremely large values, classifying ALL current-period PRs as Slow when there is no lookback data.

Suggested fix: When computeBaseline returns fallback values, return an empty/warning response or categorize all PRs as Average.

  • backend/plugins/metrics/Dockerfile:1: [medium] Dockerfile-correctness

Dockerfile uses golang:1.26-alpine as the builder image. Go 1.26 does not exist. This will cause a build failure when the image is pulled.

Suggested fix: Change the base image to match the Go version in go.mod (e.g., golang:1.21-alpine or golang:1.22-alpine).

  • backend/plugins/metrics/api/params.go:47: [medium] unbounded-request-body

DecodeQueryParams calls json.NewDecoder(r.Body).Decode without limiting body size. An attacker can send a multi-gigabyte POST body to exhaust server memory (DoS).

Suggested fix: Wrap r.Body with http.MaxBytesReader before decoding.

  • backend/plugins/metrics/api/routes/pr/handlers/utils.go:29: [medium] no-input-validation

toParams passes user-supplied values to SQL query parameters without validation. While parameterized queries prevent SQL injection, no validation on BlueprintID format, timestamp ranges, or array lengths means excessively large arrays generate long IN clauses causing query parsing overhead.

Suggested fix: Validate BlueprintID matches ^[0-9,]+$, From > 0, To > From, cap array lengths.

  • backend/plugins/metrics/api/routes/pr/handlers/key_metrics.go:33: [low] edge-case

Previous-period calculation creates a 1-second gap between periods. The From>0 guard prevents the degenerate case.

  • backend/plugins/metrics/transform/pr/zscore.go:150: [low] population-vs-sample-variance

computeBaseline uses population variance (divides by N) instead of sample variance (N-1). Impact is negligible for typical 90-day lookback but matters for small N.

  • backend/plugins/metrics/api/server.go:38: [low] race-condition

WriteJSON writes Content-Type header then encodes. If Encode fails partway, client receives partial 200 response. Standard Go HTTP limitation.

  • backend/plugins/metrics/transform/pr/zscore_test.go:48: [low] test-inadequate

TestBuildZScore_Categories uses single-PR lookback (degenerate sigma=1e-10), asserting total==2 without verifying category assignments.

  • backend/plugins/metrics/api/middleware.go:27: [low] CORS-fail-open

CORS origin is configurable via env var. If set to *, any browser origin can make requests. Default (https://devtools.pages.redhat.com) is safe.

  • backend/plugins/metrics/api/server.go:40: [low] information-disclosure

Healthz returns raw MySQL error messages to clients, potentially leaking connection details.

  • backend/plugins/metrics/cmd/main.go:57: [low] credential-handling

Default MySQL credentials could cause partial misconfiguration. MYSQL_PASS is required, mitigating the worst case.

  • backend/plugins/metrics/cmd/main.go:34: [low] no-rate-limiting

No rate limiting. The 10-connection DB pool provides implicit limiting, but explicit rate limiting is recommended before production use.

  • docs/upstream-diffs.md (file-level): Line 6 · [low] stale-enumeration

Owned plugins list does not include the new metrics plugin.

@mfrancisc mfrancisc 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.

Great Starting !

Overall the approach looks good, I think we could explore making this a "real" plugin, similar to the aireview one, as already mentioned in the PR comment.

@@ -0,0 +1,245 @@
# Metrics API

Standalone HTTP API that queries DevLake’s MySQL and returns pre-computed PR metrics for the developer dashboards. Lives under `backend/plugins/metrics/` as an independent binary (not registered in the DevLake plugin framework).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Instead of just having the code in the plugins folder but without it being registered and ran as a plugin,
we could develop a real plugin that doesn't have collector and pipeline subtasks.

A plugin can be API-only (PluginMeta + PluginApi), take a look at aireview plugin on how it does it.

Those handlers run in-process, use DevLake’s DAL/MySQL pool, and sit behind the same OIDC / API-key / CSRF stack in CreateApiServer(). So basically we also gain auth out of the box and other benefits.

Comment on lines +139 to +145
WHERE FIND_IN_SET(bp.id, ?)
AND r.name IN %s
AND a.user_name IS NOT NULL AND a.user_name != ''
AND (
(UNIX_TIMESTAMP(pr.closed_date) BETWEEN ? AND ?)
OR (UNIX_TIMESTAMP(pr.created_date) BETWEEN ? AND ?)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think we will need to exclude also comments that belong to the PR author ?

Something like a.user_name != bp.author_name ?

Comment on lines +62 to +65
outliers, err := qpr.KeyMetricsOutliers(r.Context(), db, params)
if err != nil {
log.Printf("pr/key-metrics: outliers: %v", err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think here If KeyMetricsOutliers fails, the handler logs and still returns 200 with stats (including a non-zero outlier count) and an empty drill-down list.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants