From 607739bc45ca544c1f29ac0dcd09a7f47d2b9342 Mon Sep 17 00:00:00 2001 From: Liz Tom Date: Fri, 4 Sep 2026 17:34:35 -0700 Subject: [PATCH] Fix sentry-triage bug bash issues and stream org project pagination Bug-bash fixes for the sentry-triage canvas extension: - Model list: auto-refresh via session.rpc.model.list() with a static fallback, instead of a hardcoded list that went stale. - Plain-English toggle: gate the toggle until enrichPlainEnglish() completes, fixing a race where it was interactive before enrichment finished. Add a preparing hint while it runs. - Copy: 'Plain-English titles' -> 'Plain-English messages'; simplify the loading hint. - Work status labels: distinguish 'starting Copilot fix session' vs 'filing tracking issue' instead of a generic 'working', and mark the Fix-with-Copilot toast as non-blocking. - Org project pagination: page the traversal as discrete queued tasks bounded by page count, driven by the SDK envelope hasMore flag, so a mega-org no longer starves interactive lookups and an incomplete traversal is never cached as complete. No Promise.race timeout is used because sentry@0.42.2 exposes no per-call cancellation and an abandoned call would corrupt module-global cursor state. Bump plugin to 1.2.0 and regenerate marketplace.json. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/plugin/marketplace.json | 2 +- extensions/sentry-triage/components/card.mjs | 4 +- extensions/sentry-triage/components/page.mjs | 112 ++++++++++++-- extensions/sentry-triage/extension.mjs | 146 +++++++++++++++++-- extensions/sentry-triage/package.json | 2 +- extensions/sentry-triage/sentry.mjs | 75 +++++----- extensions/sentry-triage/sentryClient.mjs | 50 +++++-- extensions/sentry-triage/server.mjs | 12 ++ extensions/sentry-triage/state.mjs | 65 ++++++++- plugins/sentry-triage/plugin.json | 2 +- 10 files changed, 386 insertions(+), 84 deletions(-) diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 8677354b0..6c0efd27c 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -1515,7 +1515,7 @@ "name": "sentry-triage", "source": "plugins/sentry-triage", "description": "Scan live Sentry issues in a Copilot canvas, group them by urgency, and hand issues off for tracking or a fix PR.", - "version": "1.1.0" + "version": "1.2.0" }, { "name": "signals-dashboard", diff --git a/extensions/sentry-triage/components/card.mjs b/extensions/sentry-triage/components/card.mjs index 4ad20d128..b8c9259fc 100644 --- a/extensions/sentry-triage/components/card.mjs +++ b/extensions/sentry-triage/components/card.mjs @@ -2,7 +2,9 @@ import { escapeHtml, safeHref } from '../escape.mjs' function statusLabel(workStatus) { if (!workStatus || typeof workStatus !== 'object') return '' - if (workStatus.phase === 'working' || workStatus.phase === 'queued') return '⏳ working…' + if (workStatus.phase === 'working' || workStatus.phase === 'queued') { + return workStatus.copilotFix ? '⏳ starting Copilot fix session…' : '⏳ filing tracking issue…' + } if (workStatus.phase === 'done') { // Numbers move into clickable links (statusLinks); keep the label a plain badge. return 'created ✓' diff --git a/extensions/sentry-triage/components/page.mjs b/extensions/sentry-triage/components/page.mjs index 46460a664..0f3589047 100644 --- a/extensions/sentry-triage/components/page.mjs +++ b/extensions/sentry-triage/components/page.mjs @@ -48,6 +48,7 @@ export function Page({ prTargets, prSettingsOpen, plainEnglishView = false, + plainEnglishEnriching = false, projects = [], availableModels = [], issueTrackers, @@ -313,14 +314,14 @@ export function Page({
-
@@ -406,6 +407,10 @@ export function Page({ // is already running for a slug, later callers (e.g. the Scan/Fetch gate) // queue here and are flushed when it settles, instead of being dropped. const projectResolveWaiters = {}; + // Ceiling for a single exact-slug lookup before we give up and report a + // transient failure. Generous enough to absorb a normal queue wait behind + // an in-progress scan, short enough that the user is never stranded. + const RESOLVE_TIMEOUT_MS = 20000; function projectResolveKey(org, slug) { return String(org || "").trim().toLowerCase() + "/" + String(slug || "").trim().toLowerCase(); } @@ -431,6 +436,10 @@ export function Page({ // An empty project scans all projects and needs no lookup. verifyProjectForScan(org, project).then((v) => { if (!v.ok) { + // "stale" means the user changed the org/project while the lookup was + // in flight — they've already moved on, so don't scan the old scope + // and don't nag them about a slug they abandoned. + if (v.reason === "stale") return; showToast(v.reason === "missing" ? "No project \u201C" + project + "\u201D in " + org + " — check the slug." : "Couldn't verify that project with Sentry — try again."); @@ -699,6 +708,7 @@ export function Page({ let lastAutoFetchedOrgDefault = ""; let currentAvailableModels = ${jsonForScript(Array.isArray(availableModels) ? availableModels : [])}; let currentPlainEnglishView = ${jsonForScript(plainEnglishView)}; + let currentPlainEnglishEnriching = ${jsonForScript(plainEnglishEnriching)}; let currentScanError = ${jsonForScript(scanError || '')}; let currentScannedTotal = ${jsonForScript(scannedTotal)}; let currentScannedCapped = ${jsonForScript(scannedCapped)}; @@ -741,7 +751,7 @@ export function Page({ function statusText(status) { if (!status || typeof status !== "object") return ""; - if (status.phase === "queued" || status.phase === "working") return "⏳ working…"; + if (status.phase === "queued" || status.phase === "working") return status.copilotFix ? "⏳ starting Copilot fix session…" : "⏳ filing tracking issue…"; if (status.phase === "skipped") return "🔒 already being worked on"; if (status.phase === "tracked") return "👀 Tracked"; if (status.phase === "done") { @@ -1089,17 +1099,34 @@ export function Page({ if (psel && psel.value !== msg.period) psel.value = msg.period; } if (Array.isArray(msg.periods)) currentPeriods = msg.periods; + if (typeof msg.plainEnglishEnriching === "boolean" && msg.plainEnglishEnriching !== currentPlainEnglishEnriching) { + currentPlainEnglishEnriching = msg.plainEnglishEnriching; + syncTitleSwitch(); + } if (Array.isArray(msg.projects)) { const forOrg = typeof msg.projectsOrg === "string" ? msg.projectsOrg.trim().toLowerCase() : ""; + const projectsComplete = msg.projectsComplete === true; + // Keep-longest ONLY for streamed/incomplete snapshots. The server + // streams partial pages (each broadcast is a growing snapshot), and a + // discovery run that fails or is cut short mid-traversal publishes a + // truncated snapshot for an org we already have fully loaded — the + // dropdown "sometimes comes back much shorter" symptom. But a COMPLETE + // traversal is authoritative and may legitimately be shorter (projects + // deleted upstream), so it must be allowed to replace the cache — + // otherwise deleted slugs would linger in autocomplete for the panel's + // whole life. Growth within a run still lands normally. + const known = forOrg ? projectsByOrg[forOrg] : null; + const regression = !projectsComplete && Array.isArray(known) && known.length > msg.projects.length; + const projects = regression ? known : msg.projects; // Always cache under the org this list belongs to so re-selecting it is // instant next time. - if (forOrg) projectsByOrg[forOrg] = msg.projects; + if (forOrg && !regression) projectsByOrg[forOrg] = msg.projects; // Only paint the on-screen field if this broadcast matches the org the // user currently has selected — a background refresh for a previous org // must not clobber the current list. const sel = selectedOrgSlug(); if (!forOrg || !sel || forOrg === sel) { - currentSentryProjects = msg.projects; + currentSentryProjects = projects; setProjectLoading(false); // Repaint whichever project field is on screen with the new options. if (currentOrg) renderProjectSwitcher(); @@ -1129,7 +1156,20 @@ export function Page({ currentSavedDefaultOrg = msg.savedDefaultOrg; refreshDefaultBtn(); } - if (Array.isArray(msg.availableModels)) currentAvailableModels = msg.availableModels; + if (Array.isArray(msg.availableModels)) { + currentAvailableModels = msg.availableModels; + // The host's model catalog can change under us (models ship or retire). + // Drop any per-card override whose id is no longer offered: otherwise the + //