Skip to content

(feat): Add e2e health data to org pulse - #1385

Open
AjayJagan wants to merge 1 commit into
red-hat-data-services:mainfrom
AjayJagan:add-e2e-health-org-pulse
Open

(feat): Add e2e health data to org pulse#1385
AjayJagan wants to merge 1 commit into
red-hat-data-services:mainfrom
AjayJagan:add-e2e-health-org-pulse

Conversation

@AjayJagan

Copy link
Copy Markdown
Contributor

Description

This implementation establishes a complete end-to-end health monitoring system for the opendatahub-operator that intelligently tracks test results across ODH and RHOAI suites. The system features smart incremental data accumulation that fetches only recent data from the Prow CI API, merges it with existing runs while preventing duplicates, and maintains a rolling 30-day dataset through automatic eviction of old entries. Key capabilities include real-time component failure analysis with contextual insights, dynamic historical trend calculation updated hourly from accumulated data, detailed test run tracking with JUnit XML parsing for failure diagnosis, and comprehensive suite health monitoring with traffic-light status indicators.

@AjayJagan
AjayJagan requested a review from accorvin as a code owner August 11, 2026 11:46
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AjayJagan's task in 2m 24s —— View job


PR Review: Add e2e health data to org pulse

  • Gather PR diff and review guidelines
  • Read review instructions and hard constraints
  • Review code changes against checklist
  • Post inline comments for specific issues
  • Post summary and verdict

This was a read-only review (fork PR). No autofixes were applied.

This PR adds a substantial E2E health monitoring system for opendatahub-operator within the system-health module. The implementation includes frontend views, backend routes, Prow CI data fetching, component failure analysis, and incremental data accumulation. Overall the feature is well-structured and thoughtfully designed, but there are several blocking issues that need to be addressed before merge.


Blocking Issues

  1. Hard constraint feat: distinguish "no GitHub username" from "0 contributions" in UI #2 — Raw filesystem access in storage.js (link)
    maintainHistoricalTrends() uses fs.readFileSync and process.cwd() to read data files directly, bypassing the readFromStorage abstraction. This violates the project's hard constraint and will break in demo mode and on the PVC mount.

  2. Hard constraint ci: Add Kind cluster smoke test workflow #3 — Heavy computation in the app backend (link)
    The backend downloads ~23MB of Prow CI data, parses it in-memory, then makes additional HTTP requests per failed job to fetch/parse JUnit XML. This is significant computation that the project conventions say belongs in an external pipeline. While this may be acceptable as a first iteration, it should be acknowledged and planned for migration.

  3. Security — No auth on POST /refresh (link)
    The POST /refresh endpoint triggers expensive external API calls but has no authentication middleware. Any unauthenticated user can trigger unlimited refreshes. Other modules in this codebase protect admin-level endpoints with requireAdmin.

  4. Bug — loadMoreRuns undefined (link)
    OdhOperatorE2eHealthView.vue references loadMoreRuns in the template but never defines it — this will throw a runtime error.

  5. Bug — Hidden route key mismatch in module.json (link)
    hiddenRoutes uses "test-run-detail" but the actual route ID is "e2e-run-detail", so the detail view will incorrectly appear in the sidebar.

  6. Hard constraint Metrics can show wrong person's work due to name resolution mismatch #7 — OpenAPI path mismatch (link)
    The @openapi annotation for the main GET endpoint uses path /api/modules/system-health/opendatahub-operator-e2e-health but the actual mounted path is /api/modules/system-health/odh-e2e-health.

Non-blocking Issues

  • Math.random() in computed property (E2eRunDetailView.vue) — Mock test details use Math.random(), causing values to flicker on every re-render. Use deterministic values based on component name/build ID.
  • hasFetched never resets on error (useOdhOperatorE2eHealth.js) — If the initial fetch fails, subsequent component mounts won't retry.
  • Memory pressure — Parsing 23MB JSON payloads hourly needs monitoring; consider streaming or If-Modified-Since caching.
  • Missing integration tests — This PR modifies module views, components, server routes, and server logic in modules/system-health/ but includes no integration test updates in tests/integration/. Per review policy, integration tests should cover sidebar visibility, view loading, content rendering, and API responses.

⚠️ Missing Integration Test Warning

Module files were modified without corresponding integration test coverage:

  • modules/system-health/client/views/OdhOperatorE2eHealthView.vue (new view)
  • modules/system-health/client/views/E2eRunDetailView.vue (new view)
  • modules/system-health/server/odh-e2e-health/routes.js (new server routes)
  • modules/system-health/server/odh-e2e-health/scheduler.js (new server logic)

Please add integration tests in tests/integration/system-health.spec.js verifying the new E2E health views load, render content, and API endpoints return expected data.


Comment on lines +289 to +293
try {
const fs = require('fs');
const path_module = require('path');
const fullPath = path_module.join(process.cwd(), 'data', 'system-health/odh-e2e-health.json');
if (fs.existsSync(fullPath)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hard constraint #2 violation (blocking): This directly reads the filesystem using fs/path and process.cwd() instead of using the readFromStorage abstraction. The storage abstractions handle demo mode, path-traversal safety, and PVC mounts.

From AGENTS.md:

Always use readFromStorage / writeToStorage for data files. Never construct raw filesystem paths.

This function receives writeToStorage but needs readFromStorage as well to load existing trends. Consider passing readFromStorage alongside writeToStorage, or refactoring so the caller passes the existing data in.

Suggested change
try {
const fs = require('fs');
const path_module = require('path');
const fullPath = path_module.join(process.cwd(), 'data', 'system-health/odh-e2e-health.json');
if (fs.existsSync(fullPath)) {
// Load existing data using storage abstraction instead of raw filesystem
// TODO: Pass readFromStorage to this function and use it here
const fs = require('fs');
const path_module = require('path');
const fullPath = path_module.join(process.cwd(), 'data', 'system-health/odh-e2e-health.json');
if (fs.existsSync(fullPath)) {
existingData = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
}

],
"hiddenRoutes": {
"disconnected-repo-detail": "component-maturity"
"disconnected-repo-detail": "component-maturity",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The hidden route key "test-run-detail" doesn't match the actual route ID "e2e-run-detail" registered in client/index.js. This means the detail view route will appear in the sidebar navigation instead of being hidden.

Should be:

Suggested change
"disconnected-repo-detail": "component-maturity",
"e2e-run-detail": "odh-e2e-health"

Comment on lines +11 to +17
Zap,
Code
} from 'lucide-vue-next'
import { apiRequest } from '@shared/client/services/api'

const nav = inject('moduleNav', null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: Math.random() is used to generate mock test details (lines ~1199-1217 in the diff). This causes the UI to show different numbers on every re-render (since this is inside a computed property), which is a poor user experience — values flicker when the component re-evaluates.

Consider computing deterministic mock values based on the component name or build ID (e.g., a simple hash function), or generating them once in onMounted instead of inside a computed.

(statusFilter.value === 'all' || run.status === statusFilter.value)
)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: loadMoreRuns is referenced in the template but never defined in the <script setup> block. This will throw a runtime error if a user clicks "Load More Runs" when the runHistory?.pagination?.hasNextPage condition is true.

Comment on lines +23 to +27
// Skip infrastructure components entirely - they are not real user-facing component failures
return component !== 'infrastructure' && stats.failureRate > 0.10;
})
.map(([component, stats]) => ({
component,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hard constraint #7 (API documentation): The @openapi annotation path is /api/modules/system-health/opendatahub-operator-e2e-health but the route is actually mounted at /api/modules/system-health/odh-e2e-health (see server/index.js line router.use('/odh-e2e-health', ...)). The OpenAPI path doesn't match the actual URL, which will cause confusion and potentially break API documentation validation.

Comment on lines +193 to +201
// Skip pending/running tests - only process completed tests
if (mappedStatus === null) {
return null;
}

return {
buildId: status.build_id || prowJob.metadata?.name || 'unknown',
jobName: job.job,
suite: suite,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Performance concern: The prowjobs.js endpoint returns ALL Prow jobs (the diff mentions ~15,000+ items) as a single JSON response. This is downloaded and parsed in-memory on every hourly refresh. At ~23MB per fetch, this will put significant memory pressure on the Node.js process, especially in a container with limited resources.

Consider:

  1. Setting a reasonable memory ceiling and monitoring for OOM
  2. Using streaming JSON parsing for very large payloads
  3. Caching the raw data and only re-fetching if newer data is available (e.g., using If-Modified-Since headers)


const cutoffTime = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1000);

for (const job of prowData.items) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Performance: For every failed job, extractFailedComponents makes HTTP requests to fetch JUnit XML from GCS. With potentially dozens of failed jobs, this creates N sequential/parallel HTTP requests during each refresh cycle. Combined with the 23MB prowjobs download, this is very heavy for a "lightweight fetching" backend (hard constraint #3: "The app is a display layer, not a compute engine").

Consider whether this JUnit parsing should happen in an external pipeline that pushes pre-computed results, consistent with the project's architecture pattern for compute-heavy operations.

Comment on lines +168 to +172
status = 'error';
logger.error('opendatahub-operator E2E health refresh failed:', err);

// For development, use demo storage if the real fetch fails and we have demo data
if (process.env.NODE_ENV !== 'production') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potential data loss: In the development error path, readFromStorage is used but it was destructured from storage at the top of the try block (line 128). If the error occurs before that destructuring succeeds (e.g., storage is undefined), this readFromStorage call will also throw, masking the original error.

Additionally, the require('./storage') call here creates a different getE2EHealthData that requires readFromStorage as a parameter — but the existing call at line 155 also passes readFromStorage from the same potentially-undefined scope.

context: "degraded",
message: "Some components showing elevated failure rates",
components: actionableComponents.slice(0, 2), // Top 2 concerning
actionRequired: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Security — No auth middleware on POST endpoint: The /refresh endpoint triggers a potentially expensive external API call (downloading 23MB from Prow, then fetching JUnit XMLs from GCS). None of the routes in this module apply requireAuth or requireAdmin. The POST /refresh endpoint should at minimum require admin authentication to prevent unauthenticated users from triggering expensive refresh operations.

Other modules in this codebase (e.g., quality, disconnected) wrap their admin-level routes with requireAdmin.

Comment on lines +3 to +8

const healthData = ref(null)
const runHistory = ref(null)
const loading = ref(false)
const error = ref(null)
let hasFetched = false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug — module-level mutable state: The healthData, runHistory, loading, error refs and the hasFetched flag are defined at module scope. This means they're shared as singletons across all components that import this composable — which is the intended behavior for a "shared store" pattern. However, the hasFetched flag is never reset, so if the initial fetch fails, calling useOdhOperatorE2eHealth() again from another component won't retry.

Consider resetting hasFetched on error, or providing a way to retry.

@AjayJagan
AjayJagan force-pushed the add-e2e-health-org-pulse branch from 9aabac0 to 98ce03d Compare August 11, 2026 11:52
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AjayJagan's task in 3m 6s —— View job


PR Review: Add e2e health data to org pulse

  • Gather PR diff and review guidelines
  • Read review instructions and hard constraints
  • Review code changes against checklist
  • Post inline comments for specific issues
  • Post summary and verdict

This was a read-only review (fork PR). No autofixes were applied.

This PR adds a substantial E2E health monitoring system for opendatahub-operator within the system-health module — frontend views, backend routes, Prow CI data fetching, component failure analysis, and incremental data accumulation. The feature is well-structured overall, but several blocking issues remain from the previous review and a few new ones were found.


Blocking Issues

  1. Hard constraint feat: distinguish "no GitHub username" from "0 contributions" in UI #2 — Raw filesystem access in storage.js:293 (comment)
    maintainHistoricalTrends() uses fs.readFileSync and process.cwd() to read data files, bypassing the readFromStorage abstraction. This breaks demo mode and PVC mounts. Fix this →

  2. Hard constraint Metrics can show wrong person's work due to name resolution mismatch #7 — OpenAPI path mismatch in routes.js:27 (comment)
    The @openapi annotation uses path /api/modules/system-health/opendatahub-operator-e2e-health but the actual mounted path is /api/modules/system-health/odh-e2e-health. Fix this →

  3. Security — No auth on POST /refresh in routes.js:69 (comment)
    The POST /refresh endpoint triggers expensive external API calls (23MB Prow download + JUnit XML fetches) without any authentication middleware. Other modules protect admin endpoints with requireAdmin. Fix this →

  4. Bug — loadMoreRuns undefined in OdhOperatorE2eHealthView.vue:370 (comment)
    Template references loadMoreRuns but it is never defined in <script setup>. Will throw a runtime error. Fix this →

  5. Bug — Hidden route key mismatch in module.json:17 (comment)
    hiddenRoutes uses key "test-run-detail" but the actual route ID is "e2e-run-detail". The detail view will appear in the sidebar. Fix this →

  6. Bug — calculateConsecutiveFailures logic error in storage.js:187 (comment)
    The function pre-filters runs to only those where the component failed, then counts "consecutive" failures — but since every run in the filtered set has the failure, it always returns the total count. It should operate on all runs sorted by time.

  7. Bug — healthCheck() missing argument in scheduler.js:540 (comment)
    getE2EHealthData() is called without the required readFromStorage parameter. Will always fail.

Non-blocking Issues

  • Math.random() in computed property (E2eRunDetailView.vue) — mockTestDetails computed uses Math.random(), causing values to flicker on re-render. Use deterministic values based on component name/index.
  • hasFetched never resets on error (useOdhOperatorE2eHealth.js:8) — If the initial fetch fails, subsequent component mounts won't retry.
  • Hard constraint ci: Add Kind cluster smoke test workflow #3 concern — Downloading ~23MB of Prow data and parsing JUnit XML per failed job is heavy for the app backend. While acceptable as a first iteration, this should be migrated to an external pipeline.
  • Memory pressure — Parsing 23MB JSON payloads hourly needs monitoring; consider streaming or If-Modified-Since caching.
  • generateMockTestSuite uses Math.random() (routes.js:502) — Server-side mock data changes on every API call. Use deterministic values.

⚠️ Missing Integration Test Warning

Module files were modified without corresponding integration test coverage:

  • modules/system-health/client/views/OdhOperatorE2eHealthView.vue (new view)
  • modules/system-health/client/views/E2eRunDetailView.vue (new view)
  • modules/system-health/server/odh-e2e-health/routes.js (new server routes)
  • modules/system-health/server/odh-e2e-health/scheduler.js (new server logic)

Please add integration tests in tests/integration/system-health.spec.js verifying the new E2E health views load, render content, and API endpoints return expected data.


const fs = require('fs');
const path_module = require('path');
const fullPath = path_module.join(process.cwd(), 'data', 'system-health/odh-e2e-health.json');
if (fs.existsSync(fullPath)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hard constraint #2 violation (blocking): maintainHistoricalTrends uses fs.readFileSync and process.cwd() to read data files directly, bypassing the readFromStorage abstraction. This function receives writeToStorage — add readFromStorage as a parameter and use it to load the existing data.

Suggested change
if (fs.existsSync(fullPath)) {
const existingData = await readFromStorage('system-health/odh-e2e-health.json');

The caller in saveE2EHealthData would also need to pass readFromStorage.

],
"hiddenRoutes": {
"disconnected-repo-detail": "component-maturity"
"disconnected-repo-detail": "component-maturity",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug (blocking): The hidden route key "test-run-detail" doesn't match the actual route ID "e2e-run-detail" registered in client/index.js. This will cause the detail view to appear in the sidebar instead of being hidden.

Suggested change
"disconnected-repo-detail": "component-maturity",
"e2e-run-detail": "odh-e2e-health"

(statusFilter.value === 'all' || run.status === statusFilter.value)
)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug (blocking): loadMoreRuns is referenced in the template but never defined in the <script setup> block. This will throw a runtime error when the "Load More Runs" button is clicked and runHistory?.pagination?.hasNextPage is true.

Either define loadMoreRuns using the existing loadRunHistory composable (with append: true), or remove this button.

return component !== 'infrastructure' && stats.failureRate > 0.10;
})
.map(([component, stats]) => ({
component,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hard constraint #7 violation (blocking): The @openapi annotation path is /api/modules/system-health/opendatahub-operator-e2e-health but the route is mounted at /api/modules/system-health/odh-e2e-health (see server/index.js line router.use('/odh-e2e-health', ...)). This mismatch will break OpenAPI validation and API documentation.

Suggested change
component,
* /api/modules/system-health/odh-e2e-health:

context: "degraded",
message: "Some components showing elevated failure rates",
components: actionableComponents.slice(0, 2), // Top 2 concerning
actionRequired: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Security issue (blocking): The POST /refresh endpoint triggers expensive external API calls (downloading ~23MB from Prow + fetching JUnit XMLs) but has no authentication middleware. The context object provides requireAuth and requireAdmin — this endpoint should use at minimum requireAdmin to prevent unauthenticated abuse.

Other modules in this codebase protect admin-level endpoints with requireAdmin.

const passRate = deduplicatedJobs.length > 0 ? passedJobs.length / deduplicatedJobs.length : 0;

const dailyStatus = calculateDailyStatus(passRate);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: calculateConsecutiveFailures pre-filters runs to only those containing the component in failedComponents, then iterates the filtered set counting consecutive failures. Since every run in the filtered set already has the component in failedComponents, the loop always returns the total count of the filtered set — it never hits the else break. The function should operate on all runs sorted by time, not just failed ones:

function calculateConsecutiveFailures(runs, component) {
  const sortedRuns = [...runs]
    .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));

  let consecutive = 0;
  for (const run of sortedRuns) {
    if (run.failedComponents && run.failedComponents.includes(component)) {
      consecutive++;
    } else {
      break;
    }
  }
  return consecutive;
}


/**
* Health check for the opendatahub-operator E2E scheduler
* @returns {Promise<Object>} Health status

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: getE2EHealthData() is called without passing readFromStorage as an argument. The function signature requires it: async function getE2EHealthData(readFromStorage). This will throw or return null, making the health check always report "no recent data".

The healthCheck function needs to accept a storage context parameter.

@AjayJagan
AjayJagan force-pushed the add-e2e-health-org-pulse branch from 98ce03d to ec75bfc Compare August 11, 2026 12:13
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AjayJagan's task in 2m 34s —— View job


PR Review: Add e2e health data to org pulse

  • Gather PR diff and review guidelines
  • Read review instructions and hard constraints
  • Review code changes against checklist
  • Post inline comments for specific issues
  • Post summary and verdict

This was a read-only review (fork PR). No autofixes were applied.

This PR adds a substantial E2E health monitoring system for opendatahub-operator within the system-health module — frontend views, backend routes, Prow CI data fetching, component failure analysis, and incremental data accumulation. The feature is well-structured and most issues from prior reviews have been addressed. Two bugs remain.


Previously Reported Issues — Now Fixed

The following issues from the prior two reviews are resolved in this push:

  • Hard constraint feat: distinguish "no GitHub username" from "0 contributions" in UI #2maintainHistoricalTrends now uses readFromStorage instead of raw fs.readFileSync / process.cwd()
  • Hard constraint Metrics can show wrong person's work due to name resolution mismatch #7 — OpenAPI path now correctly shows /api/modules/system-health/odh-e2e-health
  • Security — POST /refresh now has requireAdmin and requireScope('system-health:write') middleware
  • loadMoreRuns undefined — Function is now defined in the <script setup> block
  • Hidden route key mismatchhiddenRoutes now uses "e2e-run-detail" matching the route ID
  • Math.random() in E2eRunDetailView.vue — Replaced with deterministic hash function
  • hasFetched never resets on error — Now resets in the .catch() handler

Remaining Blocking Issues

  1. Bug — calculateConsecutiveFailures logic error in storage.js:177-192 (comment)
    The function pre-filters runs to only those where the component failed, then counts "consecutive" failures — but since every run in the filtered set has the failure, the else break is never reached. It always returns the total count of filtered runs. Should operate on all runs sorted by time (the version in component-mapper.js is correct). Fix this →

  2. Bug — healthCheck() missing readFromStorage argument in scheduler.js:540 (comment)
    getE2EHealthData() is called without the required readFromStorage parameter. Will always return null, making the health check always report "no recent data." The healthCheck function needs to accept a storage context parameter. Fix this →

Non-blocking Issues

  • Math.random() in generateMockTestSuite (routes.js:303) — Server-side mock data uses Math.random(), causing values to change on every API call. Consider deterministic values based on component name for stable dev/demo output.
  • Hard constraint ci: Add Kind cluster smoke test workflow #3 concern — Downloading ~23MB of Prow data and parsing JUnit XML per failed job is heavy for the app backend. The code acknowledges this with a detailed comment about future migration to an external pipeline — acceptable as a first iteration.

⚠️ Missing Integration Test Warning

Module files were modified without corresponding integration test coverage:

  • modules/system-health/client/views/OdhOperatorE2eHealthView.vue (new view)
  • modules/system-health/client/views/E2eRunDetailView.vue (new view)
  • modules/system-health/server/odh-e2e-health/routes.js (new server routes)
  • modules/system-health/server/odh-e2e-health/scheduler.js (new server logic)

Please add integration tests in tests/integration/system-health.spec.js verifying the new E2E health views load, render content, and API endpoints return expected data.


Comment on lines +177 to +192
.filter(run => run.failedComponents && run.failedComponents.includes(component))
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));

let consecutive = 0;
for (const run of sortedRuns) {
if (run.failedComponents && run.failedComponents.includes(component)) {
consecutive++;
} else {
break;
}
}

return consecutive;
}

/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug (blocking): calculateConsecutiveFailures pre-filters runs to only those where the component failed (.filter(run => run.failedComponents.includes(component))), then iterates the filtered set checking the same condition. Since every run in the filtered set already has the failure, the else break is never reached — the function always returns the total count of filtered runs, not actual consecutive failures.

The version in component-mapper.js is correct — it operates on all runs. This one should do the same:

Suggested change
.filter(run => run.failedComponents && run.failedComponents.includes(component))
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
let consecutive = 0;
for (const run of sortedRuns) {
if (run.failedComponents && run.failedComponents.includes(component)) {
consecutive++;
} else {
break;
}
}
return consecutive;
}
/**
function calculateConsecutiveFailures(runs, component) {
const sortedRuns = [...runs]
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
let consecutive = 0;
for (const run of sortedRuns) {
if (run.failedComponents && run.failedComponents.includes(component)) {
consecutive++;
} else {
break;
}
}
return consecutive;
}


/**
* Health check for the opendatahub-operator E2E scheduler
* @returns {Promise<Object>} Health status

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug (blocking): getE2EHealthData() requires readFromStorage as its first argument (see storage.js), but it's called here with no arguments. This will always return null, making the health check always report "no recent data."

The healthCheck function needs to accept a storage context parameter and pass readFromStorage through:

Suggested change
* @returns {Promise<Object>} Health status
const healthData = await getE2EHealthData(storage.readFromStorage);

And the function signature should accept a storage parameter.

@AjayJagan
AjayJagan force-pushed the add-e2e-health-org-pulse branch from ec75bfc to 9cc4e9c Compare August 11, 2026 12:24
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AjayJagan's task in 6m 34s —— View job


PR Review: Add e2e health data to org pulse

  • Gather PR diff and review guidelines
  • Read review instructions and hard constraints
  • Review code changes against checklist
  • Post inline comments for specific issues
  • Post summary and verdict

This was a read-only review (fork PR). No autofixes were applied.

This PR adds a substantial E2E health monitoring system for opendatahub-operator within the system-health module — frontend views, backend routes, Prow CI data fetching, component failure analysis, and incremental data accumulation. The feature is well-structured and all issues from the prior two reviews have been addressed. A few new issues were found.


Previously Reported Issues — Now Fixed

All blocking issues from the prior two reviews are resolved in this push:

  • Hard constraint feat: distinguish "no GitHub username" from "0 contributions" in UI #2maintainHistoricalTrends now uses readFromStorage instead of raw fs.readFileSync
  • Hard constraint Metrics can show wrong person's work due to name resolution mismatch #7 — OpenAPI path now correctly shows /api/modules/system-health/odh-e2e-health
  • Security — POST /refresh now has requireAdmin and requireScope('system-health:write') middleware
  • Bug — loadMoreRuns undefined — Function is now defined in the <script setup> block
  • Bug — hidden route key mismatchhiddenRoutes now uses "e2e-run-detail" matching the route ID
  • Bug — calculateConsecutiveFailures logic error — No longer pre-filters to failed-only runs; operates on all runs sorted by timestamp
  • Bug — healthCheck() missing argument — Now extracts readFromStorage from context.storage and passes it to getE2EHealthData()
  • Math.random() in computed properties — Replaced with deterministic hash function throughout
  • hasFetched never resets on error — Now resets in the .catch() handler
  • Integration tests missing — Comprehensive test suite added in tests/integration/system-health.spec.js

Non-blocking Issues

  1. req referenced before definition in timeout callbacks (prow-ci-fetch.js:358-364, routes.js:424-437)
    Both fetchJunitXml and fetchJunitXmlFromArtifacts set up a setTimeout that calls req.abort() before req is declared. Safe in practice (10-15s delay ensures assignment), but fragile if https.get() ever threw synchronously. Fix this →

  2. In-place array mutation via .sort() (storage.js:143)
    calculateConsecutiveFailures, deduplicateRunsByPRAndSuite, and deduplicateLatestPerPR all call runs.sort(...) which mutates the input array. Since calculateConsecutiveFailures is called multiple times from calculateComponentStats while iterating, this can cause unpredictable reordering. Use [...runs].sort(...) instead. Fix this →

  3. Hard constraint ci: Add Kind cluster smoke test workflow #3 concern — Downloading ~23MB of Prow data and parsing JUnit XML per failed job is heavy for the app backend. The refresh handler registration with cadence: '1h' and timeout: 300000 is reasonable, and this is acceptable as a first iteration, but should be migrated to an external pipeline if data volume grows.


Comment on lines +358 to +364
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
req.abort();
reject(new Error('JUnit XML fetch timeout'));
}, 10000); // 10 second timeout

const req = require('https').get(url, { timeout: 10000 }, (res) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The setTimeout callback on line 360 references req.abort(), but req is declared with const on line 364 — after the setTimeout. While this works in practice because the 10-second timeout fires long after req is assigned, it's fragile: if https.get() ever threw synchronously, req would remain in the temporal dead zone and the timeout callback would throw a ReferenceError.

Safer pattern — declare req before the timeout:

Suggested change
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
req.abort();
reject(new Error('JUnit XML fetch timeout'));
}, 10000); // 10 second timeout
const req = require('https').get(url, { timeout: 10000 }, (res) => {
async function fetchJunitXml(url) {
return new Promise((resolve, reject) => {
let req;
const timeout = setTimeout(() => {
if (req) req.abort();
reject(new Error('JUnit XML fetch timeout'));
}, 10000); // 10 second timeout
req = require('https').get(url, { timeout: 10000 }, (res) => {

failures: 0,
lastFailure: null,
affectedSuites: new Set()
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: runs.sort(...) mutates the input array in place. This function is called multiple times from calculateComponentStats while iterating over recentRuns — each call re-sorts the shared array, which can cause unpredictable ordering mid-iteration.

Use a copy to avoid mutating the caller's data:

Suggested change
};
const sortedRuns = [...runs].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));

The same in-place mutation issue exists in deduplicateRunsByPRAndSuite (line 103) and deduplicateLatestPerPR (line 191).

Comment on lines +424 to +437
/**
* Parse detailed test suite information from real JUnit XML artifacts
* @param {Object} runDetails - Basic run information
* @param {Function} readFromStorage - Storage read function
* @returns {Promise<Array>} Array of detailed test suite data
*/
async function parseTestSuiteDetails(runDetails, readFromStorage) {
try {
// For failed tests, try to fetch real JUnit XML from Prow artifacts
if (runDetails.status === 'failed' && runDetails.artifactsUrl) {
console.log(`Fetching real JUnit XML for build ${runDetails.buildId}`);

try {
const junitXml = await fetchJunitXmlFromArtifacts(runDetails.artifactsUrl);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same bug as fetchJunitXml: The setTimeout callback references req before it's declared on line 437. While the 15-second delay makes this safe in practice, it's fragile. Use let req before the timeout for safety.

Suggested change
/**
* Parse detailed test suite information from real JUnit XML artifacts
* @param {Object} runDetails - Basic run information
* @param {Function} readFromStorage - Storage read function
* @returns {Promise<Array>} Array of detailed test suite data
*/
async function parseTestSuiteDetails(runDetails, readFromStorage) {
try {
// For failed tests, try to fetch real JUnit XML from Prow artifacts
if (runDetails.status === 'failed' && runDetails.artifactsUrl) {
console.log(`Fetching real JUnit XML for build ${runDetails.buildId}`);
try {
const junitXml = await fetchJunitXmlFromArtifacts(runDetails.artifactsUrl);
async function fetchJunitXmlFromArtifacts(artifactsUrl) {
return new Promise((resolve, reject) => {
let req;
const timeout = setTimeout(() => {
if (req) req.abort();
reject(new Error('JUnit XML fetch timeout'));
}, 15000); // 15 second timeout for artifact fetching
// Convert artifacts URL to direct JUnit XML URL
const junitUrl = artifactsUrl.endsWith('/')
? artifactsUrl + 'artifacts/junit.xml'
: artifactsUrl + '/artifacts/junit.xml';
console.log(`Fetching JUnit XML from: ${junitUrl}`);
req = require('https').get(junitUrl, { timeout: 15000 }, (res) => {

@AjayJagan
AjayJagan force-pushed the add-e2e-health-org-pulse branch from 8e662af to 8981d89 Compare August 11, 2026 12:58
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AjayJagan's task in 3m 16s —— View job


PR Review: Add e2e health data to org pulse

  • Gather PR diff and review guidelines
  • Read review instructions and hard constraints
  • Review code changes against checklist
  • Post inline comments for specific issues
  • Post summary and verdict

This was a read-only review (fork PR). No autofixes were applied.

This PR adds a substantial E2E health monitoring system for opendatahub-operator within the system-health module — frontend views, backend routes, Prow CI data fetching, component failure analysis, and incremental data accumulation. The feature is well-structured and all blocking issues from the prior five reviews have been addressed.


Previously Reported Issues — Now Fixed

All blocking issues from the prior five reviews are resolved in this push:

  • Hard constraint feat: distinguish "no GitHub username" from "0 contributions" in UI #2maintainHistoricalTrends now uses readFromStorage instead of raw fs.readFileSync
  • Hard constraint Metrics can show wrong person's work due to name resolution mismatch #7 — OpenAPI path now correctly shows /api/modules/system-health/odh-e2e-health
  • Security — POST /refresh now has requireAdmin and requireScope('system-health:write') middleware
  • Bug — loadMoreRuns undefined — Function is now defined in the <script setup> block
  • Bug — hidden route key mismatchhiddenRoutes now uses "e2e-run-detail" matching the route ID
  • Bug — calculateConsecutiveFailures logic error — No longer pre-filters to failed-only runs; operates on all runs sorted by timestamp
  • Bug — healthCheck() missing argument — Now extracts readFromStorage from context.storage and passes it to getE2EHealthData()
  • Bug — calculateComponentStats totalRuns — Now uses recentRuns.length as denominator
  • Math.random() in computed properties — Replaced with deterministic hash function throughout (both client and server)
  • hasFetched never resets on error — Now resets in .catch() handler (applied to both composables)
  • req referenced before definition — Both prow-ci-fetch.js and routes.js now use let req; before the setTimeout
  • In-place array mutation via .sort() — All sort operations now use [...runs].sort()
  • Double download in validateProwConnection — Now uses HEAD request instead of downloading full payload
  • Variable shadowingstartTime renamed to jobStartTime inside the loop
  • Integration tests missing — Comprehensive test suite added in tests/integration/system-health.spec.js

Non-blocking Issues

  1. getOdhTrend()/getRhoaiTrend() called repeatedly in template (comment) — These functions recalculate 14-day status with filtering, deduplication, and sorting on each call (~4 times each per render). Should be cached as computed properties.

  2. Redundant historical trends computation (comment) — updateHistoricalTrends() computes 30-day trends from accumulated runs, then saveE2EHealthData() > maintainHistoricalTrends() overwrites with 14-day trends. One should be removed.

  3. console.log in client composable (comment) — Several console.log statements in useOdhOperatorE2eHealth.js will log to the user's browser console in production.

  4. Hard constraint ci: Add Kind cluster smoke test workflow #3 concern — Downloading ~23MB of Prow data and parsing JUnit XML per failed job is heavy for the app backend. The code includes a thoughtful architectural comment acknowledging this and outlining a future migration path to an external pipeline. Acceptable as a first iteration.


Comment on lines +159 to +178
return calculateCurrent14DayStatus('odh')
}

function getRhoaiTrend() {
return calculateCurrent14DayStatus('rhoai')
}

// Weekly aggregation using real Prow API data from recentRuns
const weeklyChartData = computed(() => {
// Use historical trends data instead of individual recent runs
const historicalTrends = healthData.value?.historical_trends
if (!historicalTrends?.daily_status || historicalTrends.daily_status.length === 0) {
return null
}

const dailyStatus = historicalTrends.daily_status
if (dailyStatus.length < 2) {
return null // Need at least 2 days for trend visualization
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Performance (non-blocking): getOdhTrend() and getRhoaiTrend() are called 4+ times each in the template (passRate, passedRuns, totalRuns, status). These are regular functions that recalculate 14-day status with filtering, deduplication, and sorting on each call. Consider caching as computed properties:

Suggested change
return calculateCurrent14DayStatus('odh')
}
function getRhoaiTrend() {
return calculateCurrent14DayStatus('rhoai')
}
// Weekly aggregation using real Prow API data from recentRuns
const weeklyChartData = computed(() => {
// Use historical trends data instead of individual recent runs
const historicalTrends = healthData.value?.historical_trends
if (!historicalTrends?.daily_status || historicalTrends.daily_status.length === 0) {
return null
}
const dailyStatus = historicalTrends.daily_status
if (dailyStatus.length < 2) {
return null // Need at least 2 days for trend visualization
}
const odhTrend = computed(() => calculateCurrent14DayStatus('odh'))
const rhoaiTrend = computed(() => calculateCurrent14DayStatus('rhoai'))

Then reference odhTrend / rhoaiTrend in the template instead of calling the functions repeatedly.

Comment on lines +137 to +139
logger.info('Updating historical trends from accumulated data...');
await updateHistoricalTrends(healthData, accumulatedRuns, logger);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Redundancy (non-blocking): updateHistoricalTrends() at step 10 (line 137) computes up to 30 days of daily trends from all accumulated runs. Then saveE2EHealthData() at step 11 calls maintainHistoricalTrends() which overwrites data.historical_trends with a 14-day version. The step 10 computation is wasted.

Consider removing updateHistoricalTrends() from the refresh flow and letting maintainHistoricalTrends() in saveE2EHealthData be the single source of truth for trends, or vice versa — pick one and remove the other.

Comment on lines +32 to +36
}

try {
const params = new URLSearchParams()
params.append('page', page)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cleanup (non-blocking): Several console.log statements are left in the client composable (loadHealthData, refreshData). These will log to the user's browser console in production. Consider removing them or guarding behind a dev-only check.

@AjayJagan
AjayJagan force-pushed the add-e2e-health-org-pulse branch from 8981d89 to a711201 Compare August 11, 2026 13:51
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AjayJagan's task in 4m 25s —— View job


PR Review: Add e2e health data to org pulse

  • Gather PR diff and review guidelines
  • Read review instructions and hard constraints
  • Review code changes against checklist
  • Post inline comments for specific issues
  • Post summary and verdict

This was a read-only review (fork PR). No autofixes were applied.

This PR adds a substantial E2E health monitoring system for opendatahub-operator within the system-health module — frontend views, backend routes, Prow CI data fetching, component failure analysis, and incremental data accumulation. The feature is well-structured and all blocking issues from the prior six reviews have been addressed.


Previously Reported Issues — Now Fixed

All blocking issues from the prior six reviews are resolved in this push:

  • Hard constraint feat: distinguish "no GitHub username" from "0 contributions" in UI #2maintainHistoricalTrends now uses readFromStorage instead of raw fs.readFileSync
  • Hard constraint Metrics can show wrong person's work due to name resolution mismatch #7 — OpenAPI path now correctly shows /api/modules/system-health/odh-e2e-health
  • Security — POST /refresh now has requireAdmin and requireScope('system-health:write') middleware
  • Bug — loadMoreRuns undefined — Function is now defined in the <script setup> block
  • Bug — hidden route key mismatchhiddenRoutes now uses "e2e-run-detail" matching the route ID
  • Bug — calculateConsecutiveFailures logic error — No longer pre-filters to failed-only runs; operates on all runs sorted by timestamp
  • Bug — healthCheck() missing argument — Now extracts readFromStorage from context.storage and passes it to getE2EHealthData()
  • Bug — calculateComponentStats totalRuns — Now uses recentRuns.length as denominator
  • Math.random() in computed properties — Replaced with deterministic hash function throughout (both client and server)
  • hasFetched never resets on error — Now resets in .catch() handler (applied to both composables)
  • req referenced before definition — Both prow-ci-fetch.js and routes.js now use let req; before the setTimeout
  • In-place array mutation via .sort() — All sort operations now use [...runs].sort()
  • Double download in validateProwConnection — Now uses HEAD request
  • Variable shadowingstartTime renamed to jobStartTime inside the loop
  • getOdhTrend()/getRhoaiTrend() repeated calls — Now cached as computed() properties
  • Redundant updateHistoricalTrends — Removed, maintainHistoricalTrends is now the single source
  • Integration tests missing — Comprehensive test suite added in tests/integration/system-health.spec.js

Non-blocking Issues

  1. console.log in client view (comment) — E2eRunDetailView.vue:56 has a console.log that will appear in users' browser console.

  2. Hard constraint ci: Add Kind cluster smoke test workflow #3 concern — Downloading ~23MB of Prow data and parsing JUnit XML per failed job is heavy for the app backend. The code includes a thorough architectural comment acknowledging this and outlining a future migration path to an external pipeline. Acceptable as a first iteration.

  3. Hard constraint Fix wrong-person Jira name resolution and add nickname matching #8docs/DATA-FORMATS.md doesn't document the new system-health/odh-e2e-health.json data format. The fixture file serves as implicit documentation, but the convention calls for documenting data format changes in the same PR.

  4. Missing newline at EOF in fixtures/system-health/odh-e2e-health.json and several other new files.


const detailsResponse = await apiRequest(`/modules/system-health/odh-e2e-health/runs/${buildId}/details`)
testSuites.value = detailsResponse.testSuites || []
} catch (detailError) {
console.log('Detailed test suites not available:', detailError.message)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cleanup (non-blocking): This console.log will appear in users' browser console in production. Consider removing or guarding behind a dev check.

Suggested change
console.log('Detailed test suites not available:', detailError.message)
// Detailed test suites may not be available in demo mode - this is expected

],
"last_updated": "2026-08-11T10:00:00.000Z"
}
} No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: Missing newline at end of file.

@AjayJagan
AjayJagan force-pushed the add-e2e-health-org-pulse branch from a711201 to baf69b5 Compare August 11, 2026 14:02
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AjayJagan's task in 2m 58s —— View job


PR Review: Add e2e health data to org pulse

  • Gather PR diff and review guidelines
  • Read review instructions and hard constraints
  • Review code changes against checklist
  • Post inline comments for specific issues
  • Post summary and verdict

This was a read-only review (fork PR). No autofixes were applied.

This PR adds a substantial E2E health monitoring system for opendatahub-operator within the system-health module — frontend views, backend routes, Prow CI data fetching, component failure analysis, and incremental data accumulation. The feature is well-structured and all blocking issues from the prior seven reviews have been addressed. One remaining hard constraint violation was found.


Previously Reported Issues — Now Fixed

All blocking issues from the prior seven reviews are resolved in this push:

  • Hard constraint feat: distinguish "no GitHub username" from "0 contributions" in UI #2maintainHistoricalTrends now uses readFromStorage instead of raw fs.readFileSync
  • Hard constraint Metrics can show wrong person's work due to name resolution mismatch #7 — OpenAPI path now correctly shows /api/modules/system-health/odh-e2e-health
  • Security — POST /refresh now has requireAdmin and requireScope('system-health:write') middleware
  • Bug — loadMoreRuns undefined — Function is now defined in the <script setup> block
  • Bug — hidden route key mismatchhiddenRoutes now uses "e2e-run-detail" matching the route ID
  • Bug — calculateConsecutiveFailures logic error — No longer pre-filters to failed-only runs; operates on all runs sorted by timestamp
  • Bug — healthCheck() missing argument — Now extracts readFromStorage from context.storage and passes it to getE2EHealthData()
  • Bug — calculateComponentStats totalRuns — Now uses recentRuns.length as denominator
  • Math.random() in computed properties — Replaced with deterministic hash function throughout (both client and server)
  • hasFetched never resets on error — Now resets in .catch() handler (both composables)
  • req referenced before definition — Both prow-ci-fetch.js and routes.js now use let req; before the setTimeout
  • In-place array mutation via .sort() — All sort operations now use [...runs].sort()
  • Double download in validateProwConnection — Now uses HEAD request
  • Variable shadowingstartTime renamed to jobStartTime inside the loop
  • getOdhTrend()/getRhoaiTrend() repeated calls — Now cached as computed() properties
  • Redundant updateHistoricalTrends — Removed; maintainHistoricalTrends is now the single source
  • console.log in client code — Removed from composables and views
  • Integration tests missing — Comprehensive test suite added in tests/integration/system-health.spec.js

Remaining Blocking Issue

  1. Hard constraint Fix wrong-person Jira name resolution and add nickname matching #8 — Documentation not in sync (comment)
    This PR introduces a new data format (system-health/odh-e2e-health.json) with fixture data, but docs/DATA-FORMATS.md is not updated to document its schema. Per AGENTS.md, data format changes require updating both docs/DATA-FORMATS.md and fixtures/ in the same PR. The fixture is present — please add corresponding documentation.

Non-blocking Issues

  1. Hard constraint ci: Add Kind cluster smoke test workflow #3 concern — Downloading ~23MB of Prow data and parsing JUnit XML per failed job is heavy for the app backend. The code includes a thorough architectural comment (storage.js:1-22) acknowledging this and outlining a future migration path to an external pipeline. Acceptable as a first iteration.

  2. Missing newline at EOF — Seven new files are missing a trailing newline: odh-e2e-health.json, useOdhOperatorE2eHealth.js, OdhOperatorE2eHealthView.vue, prow-ci-fetch.js, routes.js, scheduler.js, storage.js.


@@ -0,0 +1,413 @@
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hard constraint #8 (blocking): This PR introduces a new data format (system-health/odh-e2e-health.json) with fixture data, but docs/DATA-FORMATS.md is not updated to document it. Per AGENTS.md:

Documentation changes must land in the same PR as the code they describe:

  • Data format changes → update docs/DATA-FORMATS.md and fixtures/

The fixture file is correctly added — please also add the schema documentation to docs/DATA-FORMATS.md covering the top-level keys (lastSyncedAt, suites, summary, recentRuns, accumulatedRuns, componentStats, historical_trends, etc.).

@AjayJagan
AjayJagan force-pushed the add-e2e-health-org-pulse branch from baf69b5 to 6b32c5a Compare August 11, 2026 16:57
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AjayJagan's task in 3m 12s —— View job


PR Review: Add e2e health data to org pulse

  • Gather PR diff and review guidelines
  • Read review instructions and hard constraints
  • Review code changes against checklist
  • Post inline comments for specific issues
  • Post summary and verdict

This was a read-only review (fork PR). No autofixes were applied.

This PR adds a substantial E2E health monitoring system for opendatahub-operator within the system-health module — frontend views, backend routes, Prow CI data fetching, component failure analysis, and incremental data accumulation. The feature is well-structured and all blocking issues from the prior eight reviews have been addressed.


Previously Reported Issues — Now Fixed

All blocking issues from the prior eight reviews are resolved in this push:

  • Hard constraint feat: distinguish "no GitHub username" from "0 contributions" in UI #2maintainHistoricalTrends now uses readFromStorage instead of raw fs.readFileSync
  • Hard constraint Metrics can show wrong person's work due to name resolution mismatch #7 — OpenAPI path now correctly shows /api/modules/system-health/odh-e2e-health
  • Hard constraint Fix wrong-person Jira name resolution and add nickname matching #8docs/DATA-FORMATS.md updated with comprehensive schema documentation
  • Security — POST /refresh now has requireAdmin and requireScope('system-health:write') middleware
  • Bug — loadMoreRuns undefined — Function is now defined in the <script setup> block
  • Bug — hidden route key mismatchhiddenRoutes now uses "e2e-run-detail" matching the route ID
  • Bug — calculateConsecutiveFailures logic error — No longer pre-filters to failed-only runs; operates on all runs sorted by timestamp
  • Bug — healthCheck() missing argument — Now extracts readFromStorage from context.storage and passes it to getE2EHealthData()
  • Bug — calculateComponentStats totalRuns — Now uses recentRuns.length as denominator
  • Math.random() in computed properties — Replaced with deterministic hash function throughout (both client and server)
  • hasFetched never resets on error — Now resets in .catch() handler
  • req referenced before definition — Both prow-ci-fetch.js and routes.js now use let req; before the setTimeout
  • In-place array mutation via .sort() — All sort operations now use [...runs].sort()
  • Double download in validateProwConnection — Now uses HEAD request
  • Variable shadowingstartTime renamed to jobStartTime inside the loop
  • getOdhTrend()/getRhoaiTrend() repeated calls — Now cached as computed() properties
  • Redundant updateHistoricalTrends — Removed; maintainHistoricalTrends is now the single source
  • console.log in client composables — Removed
  • Integration tests missing — Comprehensive test suite added in tests/integration/system-health.spec.js

No New Blocking Issues

No blocking issues remain. All hard constraints are satisfied, security is properly handled, and the bugs from prior reviews are fixed.

Non-blocking Issues

  1. Latent type mismatch in composable computed properties (comment 1, comment 2) — currentlyBlockingComponents, overallHealth, and componentTrends in useOdhOperatorE2eHealth.js treat topFailingComponents as an array, but the API returns an object with a nested .components array. These computed properties would throw TypeError if accessed. Not currently triggered since no template uses them, but will break the moment someone does.

  2. Hard constraint ci: Add Kind cluster smoke test workflow #3 concern — Downloading ~23MB of Prow data and parsing JUnit XML per failed job is heavy for the app backend. The code includes a thoughtful architectural comment (storage.js:1-22) acknowledging this and outlining a future migration path to an external pipeline. Acceptable as a first iteration.


Comment on lines +96 to +100
const currentlyBlockingComponents = computed(() => {
if (!healthData.value?.topFailingComponents) return []
return healthData.value.topFailingComponents.filter(component =>
component.consecutiveFailures > 0
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug (non-blocking — latent, not currently triggered): topFailingComponents is an object { context, message, components, actionRequired } returned by the API (see generateContextualFailingComponents in routes.js), not an array. Calling .filter() on it would throw TypeError. Same issue affects componentTrends (line 163) which calls .reduce().

These computed properties aren't currently accessed by any template, so this won't crash the UI today — but it will the moment someone uses currentlyBlockingComponents, overallHealth, or componentTrends.

Suggested change
const currentlyBlockingComponents = computed(() => {
if (!healthData.value?.topFailingComponents) return []
return healthData.value.topFailingComponents.filter(component =>
component.consecutiveFailures > 0
)
const currentlyBlockingComponents = computed(() => {
if (!healthData.value?.topFailingComponents?.components) return []
return healthData.value.topFailingComponents.components.filter(component =>
component.consecutiveFailures > 0
)
})

Comment on lines +162 to +177
const componentTrends = computed(() => {
if (!healthData.value?.topFailingComponents) return {}

return healthData.value.topFailingComponents.reduce((trends, component) => {
trends[component.component] = {
name: component.component,
failureRate: component.failureRate,
consecutiveFailures: component.consecutiveFailures,
trend: component.failureRate > 0.3 ? 'worsening' :
component.failureRate < 0.1 ? 'improving' : 'stable',
severity: component.failureRate > 0.5 ? 'high' :
component.failureRate > 0.2 ? 'medium' : 'low'
}
return trends
}, {})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same latent bug as currentlyBlockingComponents: .reduce() on the topFailingComponents object will throw if accessed. Should use .components array:

Suggested change
const componentTrends = computed(() => {
if (!healthData.value?.topFailingComponents) return {}
return healthData.value.topFailingComponents.reduce((trends, component) => {
trends[component.component] = {
name: component.component,
failureRate: component.failureRate,
consecutiveFailures: component.consecutiveFailures,
trend: component.failureRate > 0.3 ? 'worsening' :
component.failureRate < 0.1 ? 'improving' : 'stable',
severity: component.failureRate > 0.5 ? 'high' :
component.failureRate > 0.2 ? 'medium' : 'low'
}
return trends
}, {})
})
const componentTrends = computed(() => {
if (!healthData.value?.topFailingComponents?.components) return {}
return healthData.value.topFailingComponents.components.reduce((trends, component) => {
trends[component.component] = {
name: component.component,
failureRate: component.failureRate,
consecutiveFailures: component.consecutiveFailures,
trend: component.failureRate > 0.3 ? 'worsening' :
component.failureRate < 0.1 ? 'improving' : 'stable',
severity: component.failureRate > 0.5 ? 'high' :
component.failureRate > 0.2 ? 'medium' : 'low'
}
return trends
}, {})
})

@AjayJagan
AjayJagan force-pushed the add-e2e-health-org-pulse branch from 6b32c5a to 943f5f5 Compare August 11, 2026 19:56
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AjayJagan's task in 3m 15s —— View job


PR Review: Add e2e health data to org pulse

  • Gather PR diff and review guidelines
  • Read review instructions and hard constraints
  • Review code changes against checklist
  • Post inline comments for specific issues
  • Post summary and verdict

This was a read-only review (fork PR). No autofixes were applied.

This PR adds a substantial E2E health monitoring system for opendatahub-operator within the system-health module — frontend views, backend routes, Prow CI data fetching, component failure analysis, and incremental data accumulation. The feature is well-structured and all blocking issues from the prior nine reviews have been addressed.


Previously Reported Issues — Now Fixed

All blocking issues from the prior nine reviews are resolved in this push:

  • Hard constraint feat: distinguish "no GitHub username" from "0 contributions" in UI #2maintainHistoricalTrends now uses readFromStorage instead of raw fs.readFileSync
  • Hard constraint Metrics can show wrong person's work due to name resolution mismatch #7 — OpenAPI path now correctly shows /api/modules/system-health/odh-e2e-health
  • Hard constraint Fix wrong-person Jira name resolution and add nickname matching #8docs/DATA-FORMATS.md updated with comprehensive schema documentation
  • Security — POST /refresh now has requireAdmin and requireScope('system-health:write') middleware
  • Bug — loadMoreRuns undefined — Function is now defined in the <script setup> block
  • Bug — hidden route key mismatchhiddenRoutes now uses "e2e-run-detail" matching the route ID
  • Bug — calculateConsecutiveFailures logic error — No longer pre-filters to failed-only runs; operates on all runs sorted by timestamp
  • Bug — healthCheck() missing argument — Now extracts readFromStorage from context.storage and passes it to getE2EHealthData()
  • Bug — calculateComponentStats totalRuns — Now uses recentRuns.length as denominator
  • Math.random() in computed properties — Replaced with deterministic hash function throughout (both client and server)
  • hasFetched never resets on error — Now resets in .catch() handler
  • req referenced before definitionprow-ci-fetch.js now uses let req; before the setTimeout
  • In-place array mutation via .sort() — All sort operations now use [...runs].sort()
  • Double download in validateProwConnection — Now uses HEAD request
  • Variable shadowingstartTime renamed to jobStartTime inside the loop
  • getOdhTrend()/getRhoaiTrend() repeated calls — Now cached as computed() properties
  • Redundant updateHistoricalTrends — Removed; maintainHistoricalTrends is the single source
  • console.log in client composables/views — Removed
  • Integration tests missing — Comprehensive test suite added in tests/integration/system-health.spec.js

No New Blocking Issues

No blocking issues remain. All hard constraints are satisfied, security is properly handled, and all previously identified bugs are fixed.

Non-blocking Issues

  1. Latent type mismatch in composable computed properties (comment 1, comment 2) — currentlyBlockingComponents, overallHealth, and componentTrends in useOdhOperatorE2eHealth.js treat topFailingComponents as an array, but the API returns an object with a nested .components array. These computed properties would throw TypeError if accessed. Not currently triggered since no template uses them, but will break the moment someone does. Fix this →

  2. Hard constraint ci: Add Kind cluster smoke test workflow #3 concern — Downloading ~23MB of Prow data and parsing JUnit XML per failed job is heavy for the app backend. The code includes a thoughtful architectural comment (storage.js:1-22) acknowledging this and outlining a future migration path to an external pipeline. Acceptable as a first iteration.


Comment on lines +96 to +101
const currentlyBlockingComponents = computed(() => {
if (!healthData.value?.topFailingComponents) return []
return healthData.value.topFailingComponents.filter(component =>
component.consecutiveFailures > 0
)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug (non-blocking — latent, not currently triggered): topFailingComponents is an object { context, message, components, actionRequired } from generateContextualFailingComponents() in routes.js, not an array. Calling .filter() on it throws TypeError. Same issue affects componentTrends (line 165) which calls .reduce(), and overallHealth (line 128) which reads currentlyBlockingComponents.value.length.

None of these computed properties are currently accessed by any template, so this won't crash today — but they're part of the composable's public API and will break the moment someone uses them.

Suggested change
const currentlyBlockingComponents = computed(() => {
if (!healthData.value?.topFailingComponents) return []
return healthData.value.topFailingComponents.filter(component =>
component.consecutiveFailures > 0
)
})
const currentlyBlockingComponents = computed(() => {
if (!healthData.value?.topFailingComponents?.components) return []
return healthData.value.topFailingComponents.components.filter(component =>
component.consecutiveFailures > 0
)
})

Comment on lines +162 to +177
const componentTrends = computed(() => {
if (!healthData.value?.topFailingComponents) return {}

return healthData.value.topFailingComponents.reduce((trends, component) => {
trends[component.component] = {
name: component.component,
failureRate: component.failureRate,
consecutiveFailures: component.consecutiveFailures,
trend: component.failureRate > 0.3 ? 'worsening' :
component.failureRate < 0.1 ? 'improving' : 'stable',
severity: component.failureRate > 0.5 ? 'high' :
component.failureRate > 0.2 ? 'medium' : 'low'
}
return trends
}, {})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same latent bug as currentlyBlockingComponents: .reduce() on the topFailingComponents object will throw TypeError if accessed. Should use .components array:

Suggested change
const componentTrends = computed(() => {
if (!healthData.value?.topFailingComponents) return {}
return healthData.value.topFailingComponents.reduce((trends, component) => {
trends[component.component] = {
name: component.component,
failureRate: component.failureRate,
consecutiveFailures: component.consecutiveFailures,
trend: component.failureRate > 0.3 ? 'worsening' :
component.failureRate < 0.1 ? 'improving' : 'stable',
severity: component.failureRate > 0.5 ? 'high' :
component.failureRate > 0.2 ? 'medium' : 'low'
}
return trends
}, {})
})
const componentTrends = computed(() => {
if (!healthData.value?.topFailingComponents?.components) return {}
return healthData.value.topFailingComponents.components.reduce((trends, component) => {
trends[component.component] = {
name: component.component,
failureRate: component.failureRate,
consecutiveFailures: component.consecutiveFailures,
trend: component.failureRate > 0.3 ? 'worsening' :
component.failureRate < 0.1 ? 'improving' : 'stable',
severity: component.failureRate > 0.5 ? 'high' :
component.failureRate > 0.2 ? 'medium' : 'low'
}
return trends
}, {})
})

@AjayJagan
AjayJagan force-pushed the add-e2e-health-org-pulse branch from 943f5f5 to c918b80 Compare August 12, 2026 09:56
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AjayJagan's task in 2m 23s —— View job


PR Review: Add e2e health data to org pulse

  • Gather PR diff and review guidelines
  • Read review instructions and hard constraints
  • Review code changes against checklist
  • Post inline comments for specific issues
  • Post summary and verdict

This was a read-only review (fork PR). No autofixes were applied.

This PR adds a substantial E2E health monitoring system for opendatahub-operator within the system-health module — frontend views, backend routes, Prow CI data fetching, component failure analysis, and incremental data accumulation. The feature is well-structured and all blocking issues from the prior nine reviews have been addressed.


Previously Reported Issues — Now Fixed

All blocking issues from the prior nine reviews are resolved in this push:

  • Hard constraint feat: distinguish "no GitHub username" from "0 contributions" in UI #2maintainHistoricalTrends now uses readFromStorage instead of raw fs.readFileSync
  • Hard constraint Metrics can show wrong person's work due to name resolution mismatch #7 — OpenAPI path now correctly shows /api/modules/system-health/odh-e2e-health
  • Hard constraint Fix wrong-person Jira name resolution and add nickname matching #8docs/DATA-FORMATS.md updated with comprehensive schema documentation
  • Security — POST /refresh now has requireAdmin and requireScope('system-health:write') middleware
  • Bug — loadMoreRuns undefined — Function is now defined in the <script setup> block
  • Bug — hidden route key mismatchhiddenRoutes now uses "e2e-run-detail" matching the route ID
  • Bug — calculateConsecutiveFailures logic error — No longer pre-filters to failed-only runs; operates on all runs sorted by timestamp
  • Bug — healthCheck() missing argument — Now extracts readFromStorage from context.storage and passes it to getE2EHealthData()
  • Bug — calculateComponentStats totalRuns — Now uses recentRuns.length as denominator
  • Math.random() in computed properties — Replaced with deterministic hash function throughout (both client and server)
  • hasFetched never resets on error — Now resets in .catch() handler
  • req referenced before definitionprow-ci-fetch.js now uses let req; before the setTimeout
  • In-place array mutation via .sort() — All sort operations now use [...runs].sort()
  • Double download in validateProwConnection — Now uses HEAD request
  • Variable shadowingstartTime renamed to jobStartTime inside the loop
  • getOdhTrend()/getRhoaiTrend() repeated calls — Now cached as computed() properties
  • Redundant updateHistoricalTrends — Removed; maintainHistoricalTrends is the single source
  • console.log in client composables/views — Removed
  • Integration tests missing — Comprehensive test suite added in tests/integration/system-health.spec.js

No New Blocking Issues

No blocking issues remain. All hard constraints are satisfied, security is properly handled, and all previously identified bugs are fixed.

Non-blocking Issues

  1. Latent type mismatch in composable computed properties (comment 1, comment 2) — currentlyBlockingComponents, overallHealth, and componentTrends in useOdhOperatorE2eHealth.js treat topFailingComponents as an array, but the API returns an object with a nested .components array. These computed properties would throw TypeError if accessed. Not currently triggered since no template uses them, but will break the moment someone does.

  2. Hard constraint ci: Add Kind cluster smoke test workflow #3 concern — Downloading ~23MB of Prow data and parsing JUnit XML per failed job is heavy for the app backend. The code includes a thoughtful architectural comment (storage.js:1-22) acknowledging this and outlining a future migration path to an external pipeline. Acceptable as a first iteration.


Comment on lines +96 to +101
const currentlyBlockingComponents = computed(() => {
if (!healthData.value?.topFailingComponents) return []
return healthData.value.topFailingComponents.filter(component =>
component.consecutiveFailures > 0
)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug (non-blocking — latent, not currently triggered): topFailingComponents is an object { context, message, components, actionRequired } returned by generateContextualFailingComponents() in routes.js, not an array. Calling .filter() on it will throw TypeError. Same issue affects componentTrends (line 165) which calls .reduce(), and overallHealth (line 126) which reads currentlyBlockingComponents.value.length.

None of these computed properties are currently accessed by any template, so this won't crash today — but they're part of the composable's public API and will break the moment someone uses them.

Suggested change
const currentlyBlockingComponents = computed(() => {
if (!healthData.value?.topFailingComponents) return []
return healthData.value.topFailingComponents.filter(component =>
component.consecutiveFailures > 0
)
})
const currentlyBlockingComponents = computed(() => {
if (!healthData.value?.topFailingComponents?.components) return []
return healthData.value.topFailingComponents.components.filter(component =>
component.consecutiveFailures > 0
)
})

Comment on lines +162 to +177
const componentTrends = computed(() => {
if (!healthData.value?.topFailingComponents) return {}

return healthData.value.topFailingComponents.reduce((trends, component) => {
trends[component.component] = {
name: component.component,
failureRate: component.failureRate,
consecutiveFailures: component.consecutiveFailures,
trend: component.failureRate > 0.3 ? 'worsening' :
component.failureRate < 0.1 ? 'improving' : 'stable',
severity: component.failureRate > 0.5 ? 'high' :
component.failureRate > 0.2 ? 'medium' : 'low'
}
return trends
}, {})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same latent bug as currentlyBlockingComponents: .reduce() on the topFailingComponents object will throw TypeError if accessed. Should use .components array:

Suggested change
const componentTrends = computed(() => {
if (!healthData.value?.topFailingComponents) return {}
return healthData.value.topFailingComponents.reduce((trends, component) => {
trends[component.component] = {
name: component.component,
failureRate: component.failureRate,
consecutiveFailures: component.consecutiveFailures,
trend: component.failureRate > 0.3 ? 'worsening' :
component.failureRate < 0.1 ? 'improving' : 'stable',
severity: component.failureRate > 0.5 ? 'high' :
component.failureRate > 0.2 ? 'medium' : 'low'
}
return trends
}, {})
})
const componentTrends = computed(() => {
if (!healthData.value?.topFailingComponents?.components) return {}
return healthData.value.topFailingComponents.components.reduce((trends, component) => {
trends[component.component] = {
name: component.component,
failureRate: component.failureRate,
consecutiveFailures: component.consecutiveFailures,
trend: component.failureRate > 0.3 ? 'worsening' :
component.failureRate < 0.1 ? 'improving' : 'stable',
severity: component.failureRate > 0.5 ? 'high' :
component.failureRate > 0.2 ? 'medium' : 'low'
}
return trends
}, {})
})

Signed-off-by: Ajay Jaganathan <36824134+AjayJagan@users.noreply.github.com>
@AjayJagan
AjayJagan force-pushed the add-e2e-health-org-pulse branch from c918b80 to ef475ec Compare August 12, 2026 10:22
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AjayJagan's task in 2m 31s —— View job


PR Review: Add e2e health data to org pulse

  • Gather PR diff and review guidelines
  • Read review instructions and hard constraints
  • Review code changes against checklist
  • Post inline comments for specific issues
  • Post summary and verdict

This was a read-only review (fork PR). No autofixes were applied.

This PR adds a substantial E2E health monitoring system for opendatahub-operator within the system-health module — frontend views, backend routes, Prow CI data fetching, component failure analysis, and incremental data accumulation. The feature is well-structured and all blocking issues from the prior nine reviews have been addressed.


Previously Reported Issues — Now Fixed

All blocking issues from the prior nine reviews are resolved in this push:

  • Hard constraint feat: distinguish "no GitHub username" from "0 contributions" in UI #2maintainHistoricalTrends now uses readFromStorage instead of raw fs.readFileSync
  • Hard constraint Metrics can show wrong person's work due to name resolution mismatch #7 — OpenAPI path now correctly shows /api/modules/system-health/odh-e2e-health
  • Hard constraint Fix wrong-person Jira name resolution and add nickname matching #8docs/DATA-FORMATS.md updated with comprehensive schema documentation
  • Security — POST /refresh now has requireAdmin and requireScope('system-health:write') middleware
  • Bug — loadMoreRuns undefined — Function is now defined in the <script setup> block
  • Bug — hidden route key mismatchhiddenRoutes now uses "e2e-run-detail" matching the route ID
  • Bug — calculateConsecutiveFailures logic error — No longer pre-filters to failed-only runs; operates on all runs sorted by timestamp
  • Bug — healthCheck() missing argument — Now extracts readFromStorage from context.storage and passes it to getE2EHealthData()
  • Bug — calculateComponentStats totalRuns — Now uses recentRuns.length as denominator
  • Math.random() in computed properties — Replaced with deterministic hash function throughout (both client and server)
  • hasFetched never resets on error — Now resets in .catch() handler
  • req referenced before definitionprow-ci-fetch.js now uses let req; before the setTimeout
  • In-place array mutation via .sort() — All sort operations now use [...runs].sort()
  • Double download in validateProwConnection — Now uses HEAD request
  • Variable shadowingstartTime renamed to jobStartTime inside the loop
  • getOdhTrend()/getRhoaiTrend() repeated calls — Now cached as computed() properties
  • Redundant updateHistoricalTrends — Removed; maintainHistoricalTrends is the single source
  • console.log in client composables/views — Removed
  • Integration tests missing — Comprehensive test suite added in tests/integration/system-health.spec.js

No New Blocking Issues

No blocking issues remain. All hard constraints are satisfied, security is properly handled, and all previously identified bugs are fixed.

Non-blocking Issues

  1. Latent type mismatch in useComponentAnalysis (comment) — topFailingComponents is an object { context, message, components, actionRequired }, not an array. Calling .find() on it (line 223) will throw TypeError if useComponentAnalysis is ever called. Not currently triggered by any template.

  2. Latent type mismatch in useE2EMetrics (comment) — topFailingComponents?.length (line 276) on the object is always undefined, so failingComponents is always 0. This means isHealthy and needsAttention never account for failing components. Not currently triggered by any template.

  3. Hard constraint ci: Add Kind cluster smoke test workflow #3 concern — Downloading ~23MB of Prow data and parsing JUnit XML per failed job is heavy for the app backend. The code includes a thoughtful architectural comment (storage.js:1-22) acknowledging this and outlining a future migration path to an external pipeline. Acceptable as a first iteration.



const componentData = computed(() => {
if (!healthData.value?.topFailingComponents) return null
return healthData.value.topFailingComponents.find(c => c.component === componentName)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug (non-blocking — latent, not currently triggered): topFailingComponents is an object { context, message, components, actionRequired } returned by generateContextualFailingComponents() in routes.js, not an array. Calling .find() on it will throw TypeError: topFailingComponents.find is not a function.

This function is exported but not currently called by any template, so it won't crash today — but it will the moment someone uses useComponentAnalysis.

Suggested change
return healthData.value.topFailingComponents.find(c => c.component === componentName)
if (!healthData.value?.topFailingComponents?.components) return null
return healthData.value.topFailingComponents.components.find(c => c.component === componentName)


const passRate = healthData.value.overallPassRate || 0
const totalRuns = healthData.value.totalRuns || 0
const failingComponents = healthData.value.topFailingComponents?.length || 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug (non-blocking — latent, not currently triggered): topFailingComponents is an object, not an array, so .length is undefinedfailingComponents will always be 0. This means isHealthy and needsAttention (lines 295-296) never account for failing components.

Suggested change
const failingComponents = healthData.value.topFailingComponents?.length || 0
const failingComponents = healthData.value.topFailingComponents?.components?.length || 0

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants