From 0d0b6c2e87421b1716ed79242cb39413ae1b0664 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 24 Aug 2026 15:12:31 +0000 Subject: [PATCH] fix(OUT-4107): await SWR mutate refetches and enrich fetcher errors Unawaited mutate() calls after optimistic comment, subtask, and subtemplate creation let post-mutation fetcher failures surface as unhandled rejections in Sentry. Await those mutations and include HTTP status, URL, and response body in fetcher errors to make future triage actionable. Co-authored-by: Neil Raina --- .../configure-tasks-app/ui/Subtemplates.tsx | 2 +- src/app/detail/ui/ActivityWrapper.tsx | 2 +- src/app/detail/ui/Subtasks.tsx | 2 +- src/utils/fetcher.test.ts | 35 +++++++++++++++++++ src/utils/fetcher.ts | 5 +-- 5 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/utils/fetcher.test.ts diff --git a/src/app/configure-tasks-app/ui/Subtemplates.tsx b/src/app/configure-tasks-app/ui/Subtemplates.tsx index f1742287a..22b50d1e9 100644 --- a/src/app/configure-tasks-app/ui/Subtemplates.tsx +++ b/src/app/configure-tasks-app/ui/Subtemplates.tsx @@ -90,7 +90,7 @@ export const Subtemplates = ({ template_id, token }: { template_id: string; toke const optimisticData = sortTaskByDescendingOrder([...currentSubtemplates, tempSubtemplate]) try { - mutate( + await mutate( cacheKey, async () => { const subTask = await createSubTemplate(token, template_id, payload) diff --git a/src/app/detail/ui/ActivityWrapper.tsx b/src/app/detail/ui/ActivityWrapper.tsx index 8bd1b1ff9..4aa5fd240 100644 --- a/src/app/detail/ui/ActivityWrapper.tsx +++ b/src/app/detail/ui/ActivityWrapper.tsx @@ -102,7 +102,7 @@ export const ActivityWrapper = ({ const optimisticData = getOptimisticData(postCommentPayload, activities.data, tempLog) try { - mutate( + await mutate( cacheKey, async () => { shouldRefetchRef.current = false diff --git a/src/app/detail/ui/Subtasks.tsx b/src/app/detail/ui/Subtasks.tsx index 51c45cf86..87f077855 100644 --- a/src/app/detail/ui/Subtasks.tsx +++ b/src/app/detail/ui/Subtasks.tsx @@ -104,7 +104,7 @@ export const Subtasks = ({ ) const optimisticData = subTasks?.tasks ? sortSubtasksByPriority([...subTasks.tasks, tempSubtask]) : [tempSubtask] try { - mutate( + await mutate( cacheKey, async () => { const subTask = await handleCreate(token, payload, { disableSubtaskTemplates: true }) diff --git a/src/utils/fetcher.test.ts b/src/utils/fetcher.test.ts new file mode 100644 index 000000000..9f2427efc --- /dev/null +++ b/src/utils/fetcher.test.ts @@ -0,0 +1,35 @@ +import { fetcher } from '@/utils/fetcher' + +describe('fetcher', () => { + const originalFetch = global.fetch + + afterEach(() => { + global.fetch = originalFetch + jest.restoreAllMocks() + }) + + it('returns parsed JSON for successful responses', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ tasks: [] }), + }) + + await expect(fetcher('/api/tasks/?token=test')).resolves.toEqual({ tasks: [] }) + }) + + it('throws an error with status, url, and response body for failed responses', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 500, + text: async () => 'Internal server error', + }) + + await expect(fetcher('/api/tasks/?token=test')).rejects.toThrow( + 'An error occurred while fetching the data. [500] /api/tasks/?token=test: Internal server error', + ) + }) + + it('returns undefined when url is null', async () => { + await expect(fetcher(null)).resolves.toBeUndefined() + }) +}) diff --git a/src/utils/fetcher.ts b/src/utils/fetcher.ts index 99f9dfe43..46e1b2c34 100644 --- a/src/utils/fetcher.ts +++ b/src/utils/fetcher.ts @@ -4,8 +4,9 @@ export const fetcher = async (url: string | null) => { const res = await fetch(url) if (!res.ok) { - const error = new Error('An error occurred while fetching the data.') - throw error + const responseText = await res.text().catch(() => '') + const detail = responseText ? `: ${responseText.slice(0, 200)}` : '' + throw new Error(`An error occurred while fetching the data. [${res.status}] ${url}${detail}`) } return res.json()