Skip to content

feat(#4043): add ingestion health backend API, data model, and error classification - #4250

Open
fullsend-ai-coder[bot] wants to merge 3 commits into
mainfrom
agent/4043-ingestion-health-backend
Open

feat(#4043): add ingestion health backend API, data model, and error classification#4250
fullsend-ai-coder[bot] wants to merge 3 commits into
mainfrom
agent/4043-ingestion-health-backend

Conversation

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

Implement the ingestion health backend (issue 5 of 29, RHIDP-15335 / RHIDP-15337) per the implementation gate scope:

Data model & storage:

  • boost_sync_attempts table with Knex migration, indexed on
    (connector_id, timestamp DESC)
  • SyncAttemptsRepository with insertSyncAttempt, getLatestAttempts,
    cleanupOldAttempts, and batch getLatestAttemptsForAll
  • Retention policy via boost.ingestion.healthRetention config
    (default 100 per connector) with daily scheduled cleanup

Health status API:

  • GET /api/boost/ingestion-health returns per-connector health
  • ConnectorHealthStatus type in boost-common with connectorId,
    connectorType, enabled, status, timestamps, errorSummary, metrics
  • HealthStatusService derives status from last 3 sync attempts:
    healthy (all success), degraded (mixed), failing (all failure),
    unknown (zero attempts)
  • ?includeDisabled=true query parameter support
  • ConnectorConfigReader seam for config-based connector discovery
    with dual enabled flags (startup vs runtime), ready for Connector Config — Zod Schemas and RuntimeConfigResolver Extension (issue 6 of 29) #4044
    RuntimeConfigResolver handoff
  • Structured logging via LoggerService

Error classification:

  • ErrorClassifier utility with classify(error, options?) method
  • Auth patterns (401/403, invalid token, OAuth expiry)
  • Network patterns (ECONNREFUSED, ETIMEDOUT, DNS, TLS)
  • Schema patterns (JSON parse, unexpected field, GraphQL)
  • Rate limit patterns (429, X-RateLimit, secondary limits)
  • Connector-specific matchers (GitHub, Jira, GitLab)
  • Unknown error fallback with diagnostic guidance

Explicitly out of scope per gate: RBAC gating (deferred to #4064), admin UI, force-sync, Neo4j panel, connector HealthTracker hooks, full audit event emitters.


Closes #4043

Post-script verification

  • Branch is not main/master (agent/4043-ingestion-health-backend)
  • Secret scan passed (gitleaks — 22fb4fa4a5d7785581599916da8b2dce43737be3..HEAD)
  • Pre-commit hooks passed (authoritative run on runner)
  • Tests ran inside sandbox

…classification

Implement the ingestion health backend (issue 5 of 29, RHIDP-15335 /
RHIDP-15337) per the implementation gate scope:

Data model & storage:
- boost_sync_attempts table with Knex migration, indexed on
  (connector_id, timestamp DESC)
- SyncAttemptsRepository with insertSyncAttempt, getLatestAttempts,
  cleanupOldAttempts, and batch getLatestAttemptsForAll
- Retention policy via boost.ingestion.healthRetention config
  (default 100 per connector) with daily scheduled cleanup

Health status API:
- GET /api/boost/ingestion-health returns per-connector health
- ConnectorHealthStatus type in boost-common with connectorId,
  connectorType, enabled, status, timestamps, errorSummary, metrics
- HealthStatusService derives status from last 3 sync attempts:
  healthy (all success), degraded (mixed), failing (all failure),
  unknown (zero attempts)
- ?includeDisabled=true query parameter support
- ConnectorConfigReader seam for config-based connector discovery
  with dual enabled flags (startup vs runtime), ready for #4044
  RuntimeConfigResolver handoff
- Structured logging via LoggerService

Error classification:
- ErrorClassifier utility with classify(error, options?) method
- Auth patterns (401/403, invalid token, OAuth expiry)
- Network patterns (ECONNREFUSED, ETIMEDOUT, DNS, TLS)
- Schema patterns (JSON parse, unexpected field, GraphQL)
- Rate limit patterns (429, X-RateLimit, secondary limits)
- Connector-specific matchers (GitHub, Jira, GitLab)
- Unknown error fallback with diagnostic guidance

Explicitly out of scope per gate: RBAC gating (deferred to #4064),
admin UI, force-sync, Neo4j panel, connector HealthTracker hooks,
full audit event emitters.

Closes #4043
@fullsend-ai-coder
fullsend-ai-coder Bot requested a review from durandom as a code owner August 11, 2026 12:21
@fullsend-ai-coder fullsend-ai-coder Bot added the ready-for-review Agent PR ready for human review label Aug 11, 2026
@rhdh-gh-app

rhdh-gh-app Bot commented Aug 11, 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-boost-backend workspaces/boost/plugins/boost-backend minor v0.1.4
@red-hat-developer-hub/backstage-plugin-boost-common workspaces/boost/plugins/boost-common minor v0.1.4

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.48052% with 94 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.81%. Comparing base (22fb4fa) to head (0ecc04e).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4250      +/-   ##
==========================================
+ Coverage   58.77%   58.81%   +0.03%     
==========================================
  Files        2450     2455       +5     
  Lines       97862    98170     +308     
  Branches    27310    27416     +106     
==========================================
+ Hits        57521    57735     +214     
- Misses      40061    40155      +94     
  Partials      280      280              
Flag Coverage Δ *Carryforward flag
adoption-insights 84.55% <ø> (ø) Carriedforward from 976f1e7
ai-integrations 69.76% <ø> (ø) Carriedforward from 976f1e7
app-defaults 69.79% <ø> (ø) Carriedforward from 976f1e7
augment 46.67% <ø> (ø) Carriedforward from 976f1e7
boost 76.92% <69.48%> (-0.71%) ⬇️
bulk-import 72.79% <ø> (ø) Carriedforward from 976f1e7
cost-management 13.55% <ø> (ø) Carriedforward from 976f1e7
dcm 67.21% <ø> (ø) Carriedforward from 976f1e7
e2e-intelligent-assistant 46.74% <ø> (ø) Carriedforward from 976f1e7
extensions 56.59% <ø> (ø) Carriedforward from 976f1e7
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 976f1e7
global-header 66.50% <ø> (ø) Carriedforward from 976f1e7
homepage 47.50% <ø> (ø) Carriedforward from 976f1e7
install-dynamic-plugins 59.95% <ø> (ø) Carriedforward from 976f1e7
intelligent-assistant 75.42% <ø> (ø) Carriedforward from 976f1e7
konflux 91.98% <ø> (ø) Carriedforward from 976f1e7
lightspeed 69.02% <ø> (ø) Carriedforward from 976f1e7
mcp-integrations 83.40% <ø> (ø) Carriedforward from 976f1e7
orchestrator 68.10% <ø> (ø) Carriedforward from 976f1e7
quickstart 63.74% <ø> (ø) Carriedforward from 976f1e7
sandbox 79.56% <ø> (ø) Carriedforward from 976f1e7
scorecard 86.23% <ø> (ø) Carriedforward from 976f1e7
theme 88.77% <ø> (ø) Carriedforward from 976f1e7
translations 5.12% <ø> (ø) Carriedforward from 976f1e7
x2a 79.20% <ø> (ø) Carriedforward from 976f1e7

*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 22fb4fa...0ecc04e. 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.

@mareklibra

Copy link
Copy Markdown
Member

/fs-fix add changeset and fix the CI (api-reports)

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 12:27 PM UTC · Completed 12:40 PM UTC

Commit: 005e4f8 · View workflow run →

Add changeset for boost-backend and boost-common minor version bumps.
Regenerate API report files to include new ingestion health exports
(ErrorClassifier, SyncAttemptsRepository, ConnectorConfigReader,
HealthStatusService, and related types).

Addresses review feedback on #4250
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 1 (human-triggered)

Added changeset for minor version bumps and regenerated API reports for boost-backend and boost-common to include all new ingestion health public exports. Both CI gates (changeset and api-reports) should now pass.

Fixed (2):

  1. add changeset: Added changeset file (.changeset/ingestion-health-backend.md) declaring minor version bumps for @red-hat-developer-hub/backstage-plugin-boost-backend and @red-hat-developer-hub/backstage-plugin-boost-common with a summary of the ingestion health feature.
  2. fix CI api-reports: Regenerated API report files (report.api.md) for boost-backend and boost-common to include new public exports: ErrorClassifier, SyncAttemptsRepository, ConnectorConfigReader, HealthStatusService, ConnectorHealthStatus, ErrorSummary, ErrorType, HealthStatus, SyncAttemptRecord, SyncMetrics, and related interfaces/types. Verified with 'yarn build:api-reports:only --ci'.

Tests: passed

Updated by fullsend fix agent

@mareklibra

Copy link
Copy Markdown
Member

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 11, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:30 PM UTC · Completed 1:48 PM UTC

Commit: 976f1e7 · View workflow run →

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

High

  • [rbac-violation] workspaces/boost/plugins/boost-backend/src/ingestion/routes.ts:53 — The GET /ingestion-health endpoint has no permission check. It calls httpAuth.credentials() only for logging, never calls permissions.authorize(). Every other admin-facing route in this plugin (kagenti, skills, config/status) enforces permissions via boostAdminPermission. This endpoint exposes connector health status, error messages (which may contain internal hostnames/IPs), and sync metrics to any authenticated user. IngestionHealthRoutesOptions does not even accept a PermissionsService, structurally preventing authorization without a code change. The PR body acknowledges RBAC is deferred to Ingestion Health Admin UI and Disconnected-Cluster View (issue 26 of 29) #4064, but shipping an unprotected admin endpoint creates a window where any authenticated user can access operational diagnostics.
    Remediation: Add PermissionsService to IngestionHealthRoutesOptions and enforce permissions.authorize([{ permission: boostAdminPermission }], { credentials }) before returning health data, matching the pattern in kagenti/routes.ts and skills/routes.ts.

Medium

  • [data-exposure] workspaces/boost/plugins/boost-backend/src/ingestion/routes.ts:63 — The endpoint returns raw error messages from sync failures via errorSummary.errorMessage. These can contain internal hostnames, IPs, DNS names, and API endpoint URLs from upstream connector errors (e.g., connect ECONNREFUSED 10.0.0.1:443). See also: [rbac-violation] finding — the information disclosure risk is compounded by missing authorization.
    Remediation: Return only errorType and diagnosticGuidance to non-admin callers; omit or redact raw errorMessage.

  • [logic-error] workspaces/boost/plugins/boost-backend/src/ingestion/ErrorClassifier.ts:140 — The first NETWORK_PATTERNS entry includes a bare /certificate/i regex. This matches any error containing "certificate" regardless of context, misclassifying mTLS auth failures (e.g., "Client certificate authentication required") as network errors instead of auth. The more specific patterns (UNABLE_TO_VERIFY_LEAF_SIGNATURE, self signed certificate) already cover the intended TLS cases.
    Remediation: Remove the bare /certificate/i catch-all or narrow it to TLS/SSL verification contexts (e.g., /(?:ssl|tls|x509|ca).*certificate|certificate.*(?:expired|chain|verify|untrusted)/i).

  • [edge-case] workspaces/boost/plugins/boost-backend/src/ingestion/SyncAttemptsRepository.ts:179getLatestAttemptsForAll fetches ALL rows matching connector IDs with no LIMIT clause. With default retention of 100 per connector, this returns up to 100N rows when only 3N are needed (STATUS_WINDOW=3).
    Remediation: Add .limit(connectorIds.length * limit) to the query.

  • [missing-doc] workspaces/boost/examples/app-config.connectors.yaml — New config keys boost.ingestion.healthRetention.maxAttemptsPerConnector (default: 100) and boost.connectors.<id>.enabled are read by the new code but not documented in any reference configuration.
    Remediation: Document these config keys in the reference config or a dedicated config documentation file.

Low

  • [nil-deref] workspaces/boost/plugins/boost-backend/src/ingestion/SyncAttemptsRepository.ts:137 — In insertSyncAttempt, after insert+re-read, code uses non-null assertion (created!) without a guard. Add explicit null check before rowToRecord.

  • [missing-test] workspaces/boost/plugins/boost-backend/src/ingestion/SyncAttemptsRepository.test.tscleanupOldAttempts (used in production scheduled cleanup) and getLatestAttemptsForAll (primary data access path for health API) are not unit-tested.

  • [naming-convention] workspaces/boost/plugins/boost-backend/src/ingestion/SyncAttemptsRepository.ts — Class uses Repository suffix while every other persistence class uses Store (AgentLifecycleStore, ToolLifecycleStore, McpServerStore, ConversationStore, BackendApprovalStore).

  • [pattern-inconsistency] workspaces/boost/plugins/boost-backend/src/ingestion/routes.ts — Route handler logs at info level for every GET request; other handlers use debug for reads and info only for state-changing operations.

  • [log-injection] workspaces/boost/plugins/boost-backend/src/ingestion/routes.ts:59userRef interpolated via template literal; use structured logging: logger.info('Ingestion health request', { userRef, includeDisabled }).

  • [code-organization] workspaces/boost/plugins/boost-backend/src/ingestion/SyncAttemptsRepository.tsensureTables (plural) creates only one table; other single-table stores use ensureTable (singular).


Labels: PR modifies files exclusively under workspaces/boost/ and adds a new feature (ingestion health backend API)


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.

): Router {
const { healthService, httpAuth, logger } = options;
const router = Router();

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] rbac-violation

The GET /ingestion-health endpoint has no permission check. It calls httpAuth.credentials() only for logging, never calls permissions.authorize(). Every other admin-facing route enforces permissions. IngestionHealthRoutesOptions does not accept PermissionsService.

Suggested fix: Add PermissionsService to IngestionHealthRoutesOptions and enforce permissions.authorize([{ permission: boostAdminPermission }], { credentials }) before returning health data.

| { userEntityRef?: string }
| undefined;
const userRef = principal?.userEntityRef ?? 'unknown';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] data-exposure

The endpoint returns raw error messages from sync failures via errorSummary.errorMessage. These can contain internal hostnames, IPs, DNS names, and API endpoint URLs.

Suggested fix: Return only errorType and diagnosticGuidance to non-admin callers; omit or redact raw errorMessage.

test: msg =>
/cannot\s+query\s+field/i.test(msg) ||
/field\s+'[^']+'\s+doesn'?t\s+exist\s+on\s+type/i.test(msg) ||
/graphql/i.test(msg),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] logic-error

The first NETWORK_PATTERNS entry includes a bare /certificate/i regex that matches any error containing 'certificate', misclassifying mTLS auth failures as network errors.

Suggested fix: Remove the bare /certificate/i catch-all or narrow it to TLS/SSL verification contexts.

error_message: attempt.errorMessage ?? null,
assets_added: attempt.assetsAdded ?? 0,
assets_updated: attempt.assetsUpdated ?? 0,
assets_removed: attempt.assetsRemoved ?? 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] edge-case

getLatestAttemptsForAll fetches ALL rows matching connector IDs with no LIMIT clause, returning up to 100N rows when only 3N are needed.

Suggested fix: Add .limit(connectorIds.length * limit) to the query.

connectorId: row.connector_id,
timestamp: row.timestamp,
outcome: row.outcome,
errorType: row.error_type,

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] nil-deref

In insertSyncAttempt, after insert+re-read, code uses non-null assertion (created!) without a guard.

Suggested fix: Add explicit null check: if (!created) throw new Error('Failed to read back inserted sync attempt').

try {
// Extract credentials for structured logging (task 2.6)
const credentials = await httpAuth.credentials(req);
const principal = credentials.principal as

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] log-injection

userRef interpolated via template literal; use structured logging instead.

Suggested fix: Use structured logging: logger.info('Ingestion health request', { userRef, includeDisabled }).

@fullsend-ai-review fullsend-ai-review Bot added workspace/boost Boost workspace (Backstage AI plugin) feature labels Aug 11, 2026
@mareklibra

Copy link
Copy Markdown
Member

/fs-fix

@fullsend-ai-coder

fullsend-ai-coder Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 1:51 PM UTC · Completed 2:12 PM UTC

Commit: 976f1e7 · View workflow run →

- Add PermissionsService and boostAdminPermission enforcement to
  ingestion health routes, matching the pattern used by kagenti,
  skills, and other admin routes
- Narrow ErrorClassifier certificate regex to TLS/SSL contexts,
  preventing mTLS auth errors from being misclassified as network
- Add LIMIT clause to getLatestAttemptsForAll query to prevent
  unbounded row fetches
- Replace non-null assertion with explicit null check in
  insertSyncAttempt
- Rename SyncAttemptsRepository → SyncAttemptsStore to match
  codebase naming convention (ConversationStore, McpServerStore, etc.)
- Rename ensureTables → ensureTable (single table)
- Switch route handler logging from info to debug with structured
  fields (matches read-handler convention)
- Add tests for cleanupOldAttempts and getLatestAttemptsForAll
- Add test for certificate regex narrowing
- Add permission denial test for routes
- Document new config keys in example config

Addresses review feedback on #4250
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 2 (human-triggered)

Addressed 10 of 11 review findings. Fixed: RBAC enforcement (high), certificate regex logic error, unbounded query, null-safety, missing tests, naming convention, log level, structured logging, method naming, config documentation. Disagreed with 1 (data-exposure): raw error messages are appropriate for admin-only access now that RBAC is enforced.

Fixed (10):

  1. rbac-violation: missing permission check on /ingestion-health (workspaces/boost/plugins/boost-backend/src/ingestion/routes.ts): Added PermissionsService to IngestionHealthRoutesOptions and enforced boostAdminPermission authorization before returning health data, matching the pattern used by kagenti/routes.ts and skills/routes.ts. Updated plugin.ts to pass permissions service.
  2. logic-error: bare /certificate/i regex too broad in ErrorClassifier (workspaces/boost/plugins/boost-backend/src/ingestion/ErrorClassifier.ts): Replaced bare /certificate/i catch-all with narrower regex /(?:ssl|tls|x509|ca).certificate|certificate.(?:expired|chain|verify|untrusted)/i that only matches TLS/SSL certificate contexts. Added test confirming 'Client certificate authentication required' is no longer misclassified as network.
  3. edge-case: getLatestAttemptsForAll fetches all rows without LIMIT (workspaces/boost/plugins/boost-backend/src/ingestion/SyncAttemptsStore.ts): Added .limit(connectorIds.length * limit) to the query to cap total rows fetched, preventing unbounded reads with many connectors.
  4. missing-doc: new config keys not documented (workspaces/boost/examples/app-config.connectors.yaml): Added Ingestion Health Configuration section documenting boost.ingestion.healthRetention.maxAttemptsPerConnector (default: 100) and the per-connector enabled flag pattern.
  5. nil-deref: non-null assertion in insertSyncAttempt (workspaces/boost/plugins/boost-backend/src/ingestion/SyncAttemptsStore.ts): Replaced created! non-null assertion with explicit null check that throws a descriptive error if the re-read fails.
  6. missing-test: cleanupOldAttempts and getLatestAttemptsForAll not tested (workspaces/boost/plugins/boost-backend/src/ingestion/SyncAttemptsStore.test.ts): Added test suites for getLatestAttemptsForAll (3 tests: map return, empty list, per-connector limit) and cleanupOldAttempts (2 tests: delete beyond retention, no-op under limit).
  7. naming-convention: Repository suffix instead of Store (workspaces/boost/plugins/boost-backend/src/ingestion/SyncAttemptsStore.ts): Renamed SyncAttemptsRepository to SyncAttemptsStore and SyncAttemptsRepositoryOptions to SyncAttemptsStoreOptions across all files (class, interface, imports, tests, barrel exports, plugin.ts, index.ts). File renamed from SyncAttemptsRepository.ts to SyncAttemptsStore.ts.
  8. pattern-inconsistency: route handler logs at info level for GET (workspaces/boost/plugins/boost-backend/src/ingestion/routes.ts): Changed logger.info to logger.debug for the GET /ingestion-health handler, matching the convention where read handlers use debug and state-changing operations use info.
  9. log-injection: userRef interpolated via template literal (workspaces/boost/plugins/boost-backend/src/ingestion/routes.ts): Replaced template literal logger.info(Ingestion health request by ${userRef}...) with structured logging logger.debug('Ingestion health request', { userRef, includeDisabled }).
  10. code-organization: ensureTables (plural) creates only one table (workspaces/boost/plugins/boost-backend/src/ingestion/SyncAttemptsStore.ts): Renamed private method ensureTables to ensureTable (singular), matching the single-table semantics.

Disagreed (1):

  1. data-exposure: raw error messages in API response: With RBAC enforcement now in place (rbac-violation fix), only admin users can access the /ingestion-health endpoint. Raw error messages containing internal hostnames/IPs are appropriate operational diagnostics for admin users. Redacting errorMessage for admin callers would reduce the diagnostic value of the endpoint. The data exposure risk identified by the reviewer was compounded by the missing authorization — that root cause is now addressed.

Tests: passed

Updated by fullsend fix agent

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

feature ready-for-review Agent PR ready for human review workspace/boost Boost workspace (Backstage AI plugin)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ingestion Health — API, Data Model, Error Classification (issue 5 of 29)

1 participant