diff --git a/apps/server/src/api/v2/metric_files/download-metric-file.test.ts b/apps/server/src/api/v2/metric_files/download-metric-file.test.ts index 62a843221..24e9b7ada 100644 --- a/apps/server/src/api/v2/metric_files/download-metric-file.test.ts +++ b/apps/server/src/api/v2/metric_files/download-metric-file.test.ts @@ -121,12 +121,19 @@ describe('downloadMetricFileHandler', () => { organizationId: mockOrganizationId, }); - // Verify task was triggered with correct parameters - expect(tasks.trigger).toHaveBeenCalledWith('export-metric-data', { - metricId: mockMetricId, - userId: mockUser.id, - organizationId: mockOrganizationId, - }); + // Verify task was triggered with correct parameters and idempotency + expect(tasks.trigger).toHaveBeenCalledWith( + 'export-metric-data', + { + metricId: mockMetricId, + userId: mockUser.id, + organizationId: mockOrganizationId, + }, + { + idempotencyKey: `export-${mockUser.id}-${mockMetricId}`, + idempotencyKeyTTL: '5m', + } + ); // Verify successful response expect(result).toMatchObject({ diff --git a/apps/server/src/api/v2/metric_files/download-metric-file.ts b/apps/server/src/api/v2/metric_files/download-metric-file.ts index 45d7e1856..1e6eacff9 100644 --- a/apps/server/src/api/v2/metric_files/download-metric-file.ts +++ b/apps/server/src/api/v2/metric_files/download-metric-file.ts @@ -50,12 +50,21 @@ export async function downloadMetricFileHandler( } try { - // Trigger the export task - const handle = await tasks.trigger('export-metric-data', { - metricId, - userId: user.id, - organizationId, - }); + // Trigger the export task with idempotency to prevent duplicates + // If the same user tries to download the same metric within 5 minutes, + // it will return the existing task instead of creating a new one + const handle = await tasks.trigger( + 'export-metric-data', + { + metricId, + userId: user.id, + organizationId, + }, + { + idempotencyKey: `export-${user.id}-${metricId}`, + idempotencyKeyTTL: '5m', // 5 minutes TTL + } + ); // Poll for task completion with timeout const startTime = Date.now(); diff --git a/apps/web/src/controllers/MetricController/MetricViewChart/MetricDataTruncatedWarning.tsx b/apps/web/src/controllers/MetricController/MetricViewChart/MetricDataTruncatedWarning.tsx index c522d316b..1d28a80d4 100644 --- a/apps/web/src/controllers/MetricController/MetricViewChart/MetricDataTruncatedWarning.tsx +++ b/apps/web/src/controllers/MetricController/MetricViewChart/MetricDataTruncatedWarning.tsx @@ -22,9 +22,9 @@ export const MetricDataTruncatedWarning: React.FC { - setTimeout(() => reject(new Error('Download timeout')), 3 * 60 * 1000); // 3 minutes + setTimeout(() => reject(new Error('Download timeout')), 2 * 60 * 1000); // 2 minutes }); // Race between the API call and the timeout @@ -36,14 +36,17 @@ export const MetricDataTruncatedWarning: React.FC { + setIsDownloading(false); + }, 5000); } catch (error) { console.error('Failed to download metric file:', error); setHasError(true); - } finally { - // Add a small delay before removing loading state since download happens async - setTimeout(() => { - setIsDownloading(false); - }, 1000); + // Re-enable button immediately on error so user can retry + setIsDownloading(false); } };