Skip to content

Commit 4467d68

Browse files
committed
fix(dashboard): preserve last-known PR enrichment when backfill fails
Poll cycles fully replaced the PR list each cycle, so a transient enrichment-backfill failure (network blocker, blip, or GitHub hiccup) wiped size/check-status data and reclassified dependency PRs from Mergeable to Needs Action until the next successful poll. Add fallbackToPreviousEnrichment to carry forward a PR's prior enriched fields when the current cycle failed to re-enrich it, and guard the fine-grained merge path against the same regression. Add warn + Sentry telemetry to the backfill catch block to match its sibling fetchers.
1 parent 42d002f commit 4467d68

2 files changed

Lines changed: 57 additions & 14 deletions

File tree

‎src/app/components/dashboard/DashboardPage.tsx‎

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import DependenciesTab from "./DependenciesTab";
1414
import { isDependencyPr, expandBotLogins, needsBodyFallback, parseRenovateBody, type VersionInfo } from "../../lib/dependency-detection";
1515
import { isRepoExcludedFromDependencies } from "../../lib/dependency-exclusion";
1616
import { findDashboardIssues, parseAbandonedSection, resetAbandonedPatternCache, type AbandonedDependency } from "../../lib/dependency-dashboard";
17-
import { fetchDashboardIssueBodies, fetchDepPRBodies } from "../../services/api";
17+
import { fetchDashboardIssueBodies, fetchDepPRBodies, fallbackToPreviousEnrichment } from "../../services/api";
1818
import type { SortOption } from "../shared/SortDropdown";
1919
import type { Issue, PullRequest, WorkflowRun } from "../../services/api";
2020
import { fetchOrgs } from "../../services/api";
@@ -285,23 +285,29 @@ async function pollFetch(): Promise<DashboardData> {
285285
for (let i = 0; i < state.pullRequests.length; i++) {
286286
const e = enrichedMap.get(state.pullRequests[i].id)!;
287287
const pr = state.pullRequests[i];
288-
pr.headSha = e.headSha;
289-
pr.assigneeLogins = e.assigneeLogins;
290-
pr.reviewerLogins = e.reviewerLogins;
291-
pr.checkStatus = e.checkStatus;
292-
pr.additions = e.additions;
293-
pr.deletions = e.deletions;
294-
pr.changedFiles = e.changedFiles;
295-
pr.comments = e.comments;
296-
pr.reviewThreads = e.reviewThreads;
297-
pr.totalReviewCount = e.totalReviewCount;
298-
pr.enriched = e.enriched;
288+
// A failed backfill batch returns e.enriched === false for PRs it
289+
// couldn't reach — don't let that regress a PR that was already
290+
// enriched from a prior cycle.
291+
const regressing = e.enriched === false && pr.enriched !== false;
292+
if (!regressing) {
293+
pr.headSha = e.headSha;
294+
pr.assigneeLogins = e.assigneeLogins;
295+
pr.reviewerLogins = e.reviewerLogins;
296+
pr.checkStatus = e.checkStatus;
297+
pr.additions = e.additions;
298+
pr.deletions = e.deletions;
299+
pr.changedFiles = e.changedFiles;
300+
pr.comments = e.comments;
301+
pr.reviewThreads = e.reviewThreads;
302+
pr.totalReviewCount = e.totalReviewCount;
303+
pr.enriched = e.enriched;
304+
}
299305
pr.nodeId = e.nodeId;
300306
pr.surfacedBy = e.surfacedBy;
301307
pr.starCount = e.starCount;
302308
}
303309
} else {
304-
state.pullRequests = data.pullRequests;
310+
state.pullRequests = fallbackToPreviousEnrichment(state.pullRequests, data.pullRequests);
305311
}
306312
}));
307313
} else {
@@ -310,10 +316,11 @@ async function pollFetch(): Promise<DashboardData> {
310316
// changed since the last cycle. Preserve scroll position: SolidJS
311317
// DOM updates are synchronous within the setter, so save/restore
312318
// around it to prevent scroll reset from <For> DOM rebuild.
319+
const pullRequests = fallbackToPreviousEnrichment(dashboardData.pullRequests, data.pullRequests);
313320
withScrollLock(() => {
314321
setDashboardData({
315322
issues: data.issues,
316-
pullRequests: data.pullRequests,
323+
pullRequests,
317324
workflowRuns: config.enableActions ? data.workflowRuns : [],
318325
loading: false,
319326
lastRefreshedAt: now,

‎src/app/services/api.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,8 @@ export async function fetchPREnrichment(
11511151
updateGraphqlRateLimit(partialErr.rateLimit);
11521152
}
11531153
const { statusCode, message } = extractRejectionError(err);
1154+
console.warn(`[api] PR enrichment batch ${batchIdx + 1}/${batches.length} failed:`, err);
1155+
Sentry.captureException(err, { tags: { source: "prEnrichment" } });
11541156
errors.push({
11551157
repo: `backfill-batch-${batchIdx + 1}/${batches.length}`,
11561158
statusCode, message,
@@ -1361,6 +1363,40 @@ function mergeEnrichment(
13611363
});
13621364
}
13631365

1366+
/**
1367+
* Carries forward a PR's last-known enrichment when this cycle's backfill
1368+
* failed for it, instead of regressing an already-enriched PR to unenriched.
1369+
* Without this, a single transient backfill failure wipes size/check-status
1370+
* data and can flip dependency-status classification (e.g. Mergeable ->
1371+
* Needs Action) until the next successful poll re-enriches it.
1372+
*/
1373+
export function fallbackToPreviousEnrichment(
1374+
previous: PullRequest[],
1375+
next: PullRequest[]
1376+
): PullRequest[] {
1377+
if (previous.length === 0) return next;
1378+
const previousMap = new Map(previous.map((pr) => [pr.id, pr]));
1379+
return next.map((pr) => {
1380+
if (pr.enriched !== false) return pr;
1381+
const prev = previousMap.get(pr.id);
1382+
if (!prev || prev.enriched === false) return pr;
1383+
return {
1384+
...pr,
1385+
headSha: prev.headSha,
1386+
assigneeLogins: prev.assigneeLogins,
1387+
reviewerLogins: prev.reviewerLogins,
1388+
checkStatus: prev.checkStatus,
1389+
additions: prev.additions,
1390+
deletions: prev.deletions,
1391+
changedFiles: prev.changedFiles,
1392+
comments: prev.comments,
1393+
reviewThreads: prev.reviewThreads,
1394+
totalReviewCount: prev.totalReviewCount,
1395+
enriched: true,
1396+
};
1397+
});
1398+
}
1399+
13641400
/**
13651401
* Merges tracked user search results into the main issue/PR maps.
13661402
* Items already present get the tracked user's login appended to surfacedBy.

0 commit comments

Comments
 (0)