debug: log Kanban state-change DnD flow end-to-end (#391)#392
Open
BenGWeeks wants to merge 1 commit into
Open
Conversation
When a card is dragged between columns, the persisted state change can silently fail (the card snaps back) — e.g. dragging New -> To Do. The underlying updateTicketState only threw response.statusText, so the actual Azure DevOps rejection reason was lost. Add structured logging at every hop so we can see exactly why a transition is being rejected: - KanbanBoard.handleDragEnd: log itemId, fromState, targetState, whether a handler is wired, and the rollback path on failure. - /kanban handleStateChange: log the PATCH and capture the response body when the API rejects the call. - /api/devops/tickets/[id]/state route: log incoming payload, success branch, and the DevOps error; return 400 with the underlying error message instead of a generic 500 so the client sees the real reason. - AzureDevOpsService.updateTicketState: read the DevOps error body (TF237124 etc.) and include it in the thrown error. Diagnostic only. No behavioural change to successful flows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds investigative logging across the Kanban state-change drag-and-drop flow (component → page handler → API route → DevOps service) to diagnose issue #391, where dragging a card from "New" to "To Do" silently fails. Also captures and propagates the underlying Azure DevOps error body instead of dropping it, and changes the API route to return 400 with the real message instead of a generic 500.
Changes:
- Add structured
[Kanban DnD]/[Kanban]/[state PATCH]/[devops.updateTicketState]console logs at every hop of the state-change flow. - Capture the DevOps error response body in
AzureDevOpsService.updateTicketStateand embed it in the thrown error. - Wrap
updateTicketStatecalls inroute.tswith try/catch, returning 400 with the real error message.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/lib/devops.ts | Logs PATCH request, reads error body and includes it in the thrown error. |
| src/components/tickets/KanbanBoard.tsx | Adds drag-end/handler/success/failure logs around onTicketStateChange. |
| src/app/kanban/page.tsx | Logs the outgoing PATCH and reads/logs the response body on failure. |
| src/app/api/devops/tickets/[id]/state/route.ts | Adds entry/success/failure logs and returns 400 with the upstream error message. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
49
to
+100
| @@ -42,33 +65,50 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< | |||
| try { | |||
| const workItem = await devopsService.getWorkItem(project.name, ticketId); | |||
| if (workItem) { | |||
| const updatedWorkItem = await devopsService.updateTicketState( | |||
| project.name, | |||
| ticketId, | |||
| state | |||
| ); | |||
| try { | |||
| const updatedWorkItem = await devopsService.updateTicketState( | |||
| project.name, | |||
| ticketId, | |||
| state | |||
| ); | |||
| console.log('[state PATCH] success (fallback project lookup)', { | |||
| ticketId, | |||
| state, | |||
| project: project.name, | |||
| }); | |||
|
|
|||
| const ticket = workItemToTicket(updatedWorkItem, { | |||
| id: project.id, | |||
| name: project.name, | |||
| devOpsProject: project.name, | |||
| devOpsOrg: organization || '', | |||
| tags: [], | |||
| createdAt: new Date(), | |||
| updatedAt: new Date(), | |||
| }); | |||
| const ticket = workItemToTicket(updatedWorkItem, { | |||
| id: project.id, | |||
| name: project.name, | |||
| devOpsProject: project.name, | |||
| devOpsOrg: organization || '', | |||
| tags: [], | |||
| createdAt: new Date(), | |||
| updatedAt: new Date(), | |||
| }); | |||
|
|
|||
| return NextResponse.json({ ticket }); | |||
| return NextResponse.json({ ticket }); | |||
| } catch (err) { | |||
| console.error('[state PATCH] DevOps rejected update (fallback)', { | |||
| ticketId, | |||
| state, | |||
| project: project.name, | |||
| error: err instanceof Error ? err.message : err, | |||
| }); | |||
| const message = err instanceof Error ? err.message : 'Failed to update ticket state'; | |||
| return NextResponse.json({ error: message }, { status: 400 }); | |||
| } | |||
| body: errorBody, | ||
| }); | ||
| throw new Error( | ||
| `Failed to update work item ${workItemId} state to "${state}": ${response.status} ${response.statusText} — ${errorBody}` |
Comment on lines
244
to
+286
| @@ -245,14 +260,30 @@ export default function KanbanBoard({ | |||
| if (onTicketStateChange) { | |||
| setIsUpdating(true); | |||
| try { | |||
| console.log('[Kanban DnD] calling onTicketStateChange', { | |||
| itemId: activeItemId, | |||
| fromState, | |||
| targetState, | |||
| }); | |||
| await onTicketStateChange(activeItemId, targetState); | |||
| console.log('[Kanban DnD] state change succeeded', { | |||
| itemId: activeItemId, | |||
| targetState, | |||
| }); | |||
| } catch (error) { | |||
| console.error('Failed to update item state:', error); | |||
| console.error('[Kanban DnD] state change failed — rolling back', { | |||
| itemId: activeItemId, | |||
| fromState, | |||
| targetState, | |||
| error, | |||
| }); | |||
| // Rollback on failure | |||
| setLocalItems(sourceItems); | |||
| } finally { | |||
| setIsUpdating(false); | |||
| } | |||
| } else { | |||
| console.warn('[Kanban DnD] no onTicketStateChange handler wired — change will not persist'); | |||
Comment on lines
+1218
to
+1228
| const errorBody = await response.text().catch(() => '<no body>'); | ||
| console.error('[devops.updateTicketState] DevOps rejected PATCH', { | ||
| projectName, | ||
| workItemId, | ||
| state, | ||
| status: response.status, | ||
| statusText: response.statusText, | ||
| body: errorBody, | ||
| }); | ||
| throw new Error( | ||
| `Failed to update work item ${workItemId} state to "${state}": ${response.status} ${response.statusText} — ${errorBody}` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AzureDevOpsService.updateTicketStatewas only throwingresponse.statusText, dropping DevOps's real error body (commonlyTF237124: An invalid state transition from X to Y was requested). That's now captured and propagated all the way back to the browser console./api/devops/tickets/[id]/stateroute now returns 400 with the real error message instead of a generic 500, so the client sees the cause.How to use
bun dev, open the Kanban board with DevTools console open (and the server log visible).[Kanban DnD]log line at drag-end (shows fromState/targetState/known states).[Kanban]PATCH log + the[Kanban] state PATCH rejectedlog with status + body.[state PATCH]and[devops.updateTicketState]logs.Test plan
bun run checkpasses locally (done).🤖 Generated with Claude Code