Detail Bug Report
https://app.detail.dev/org_3377c26d-da48-4ccd-b83a-22c542f4fe83/bugs/bug_d897e247-f5dc-438c-843b-8566d68c4a86
Introduced in #26765 by @ShaileshParmar11 on Mar 28, 2026
Summary
- Context:
IncidentTypeAreaChartWidget renders the "Open Incidents" / "Resolved Incidents" cards on the Data Quality dashboard, fetching per-day incident counts via fetchCountOfIncidentStatusTypeByDays whenever the dashboard's chartFilter prop changes.
- Bug: The
useEffect that triggers getCountOfIncidentStatus has no AbortController and no ignore-stale-response guard, so when chartFilter changes while a previous request is still in flight, the stale response can resolve after the fresh one and overwrite the correct data with data for a filter the user no longer has selected.
- Actual vs. expected: After quickly changing a dashboard filter, the card's number and area-chart trend should reflect the newly-selected filter; instead it can display the counts for the previously-selected (now stale) filter, persisted in component state until the next filter change or re-fetch.
- Impact: The wrong data is confined to this widget's in-card number/trend display. The card's click-through
redirectPath is built by the parent (DqDashboardSectionContent.component.tsx) from the current defaultFilters prop, not from the widget's local chartData, so navigation always lands on the incident manager scoped to the correct date range.
Code with Bug
openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTypeAreaChartWidget/IncidentTypeAreaChartWidget.component.tsx:
const getCountOfIncidentStatus = async () => {
setIsChartLoading(true);
try {
const { data } = await fetchCountOfIncidentStatusTypeByDays(
incidentStatusType,
chartFilter
);
const updatedData = data.map((item) => ({
timestamp: +item.timestamp,
count: +item.stateId,
}));
setChartData(updatedData); // <-- BUG 🔴 no guard against resolving for a stale chartFilter
} catch {
setChartData([]);
} finally {
setIsChartLoading(false);
}
};
useEffect(() => {
getCountOfIncidentStatus(); // <-- BUG 🔴 no ignore-stale guard / no cleanup return
}, [chartFilter, incidentStatusType]);
Explanation
- Every dashboard filter change creates a new
chartFilter object identity and re-triggers the effect.
- Multiple in-flight requests can overlap;
setChartData applies whichever response resolves last, even if it corresponds to an older filter.
- Resulting behavior: rapid filter A → filter B changes can briefly show B, then revert to stale A when request A resolves after request B.
Codebase Inconsistency
The click-through URL is constructed by the parent from current defaultFilters, not the widget’s potentially stale chartData, so navigation remains correct even when the card display is wrong:
openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DqDashboard/DqDashboardSectionContent/DqDashboardSectionContent.component.tsx:
<IncidentTypeAreaChartWidget
chartFilter={defaultFilters}
height={60}
incidentStatusType={TestCaseResolutionStatusTypes.New}
name="open-incident"
redirectPath={{
pathname: incidentPath,
search: QueryString.stringify({
testCaseResolutionStatusType: TestCaseResolutionStatusTypes.New,
startTs: defaultFilters.startTs,
endTs: defaultFilters.endTs,
}),
}}
title={t('label.open-incident-plural')}
/>
Failing Test
import '@testing-library/jest-dom/extend-expect';
import { act, render, screen } from '@testing-library/react';
import { TestCaseResolutionStatusTypes } from '../../../../generated/tests/testCaseResolutionStatus';
import { fetchCountOfIncidentStatusTypeByDays } from '../../../../rest/dataQualityDashboardAPI';
import { IncidentTypeAreaChartWidgetProps } from '../../DataQuality.interface';
import IncidentTypeAreaChartWidget from './IncidentTypeAreaChartWidget.component';
jest.mock('../../../../rest/dataQualityDashboardAPI', () => ({
fetchCountOfIncidentStatusTypeByDays: jest.fn(),
}));
jest.mock('../../../Visualisations/Chart/CustomAreaChart.component', () =>
jest.fn().mockImplementation(() => <div>CustomAreaChart.component</div>)
);
const defaultProps: IncidentTypeAreaChartWidgetProps = {
incidentStatusType: TestCaseResolutionStatusTypes.New,
name: 'Incident Type',
title: 'Incident Type Area Chart Widget',
};
describe('IncidentTypeAreaChartWidget race repro', () => {
it('should display the latest chartFilter data when an older request resolves after a newer one', async () => {
let resolveA: (v: { data: Array<{ stateId: string; timestamp: string }> }) => void = () => {};
let resolveB: (v: { data: Array<{ stateId: string; timestamp: string }> }) => void = () => {};
const fetchMock = fetchCountOfIncidentStatusTypeByDays as jest.Mock;
fetchMock.mockImplementation((_status: unknown, filters: any) => {
if (filters?.startTs === 1) {
return new Promise((r) => { resolveA = r; });
}
return new Promise((r) => { resolveB = r; });
});
const filterA = { startTs: 1, endTs: 10 };
const filterB = { startTs: 100, endTs: 200 };
const { rerender } = render(
<IncidentTypeAreaChartWidget {...defaultProps} chartFilter={filterA} />
);
// Re-render with the new filter before the first request resolves.
rerender(<IncidentTypeAreaChartWidget {...defaultProps} chartFilter={filterB} />);
// Resolve the newer request first, then the older one, inside act() for determinism.
await act(async () => {
resolveB({ data: [{ stateId: '200', timestamp: '1729468800000' }] });
resolveA({ data: [{ stateId: '1', timestamp: '1729468800000' }] });
});
// Expected: latest filter (B) value '200'. Actual (buggy): stale A value '1'.
expect(screen.getByTestId('total-value').textContent).toEqual('200');
});
});
Run against the unmodified component (FAIL):
FAIL @openmetadata src/components/DataQuality/ChartWidgets/IncidentTypeAreaChartWidget/IncidentTypeAreaChartWidget.race.repro.test.tsx (8.211 s)
IncidentTypeAreaChartWidget race repro
✕ should display the latest chartFilter data when an older request resolves after a newer one (43 ms)
● IncidentTypeAreaChartWidget race repro › should display the latest chartFilter data when an older request resolves after a newer one
expect(received).toEqual(expected) // deep equality
Expected: "200"
Received: "1"
46 |
47 | // Expected: latest filter (B) value '200'. Actual (buggy): stale A value '1'.
> 48 | expect(screen.getByTestId('total-value').textContent).toEqual('200');
| ^
49 | });
50 | });
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Recommended Fix
Add an effect cleanup “ignore stale resolution” guard (rather than AbortController, because requests may be coalesced by the batcher):
useEffect(() => {
let active = true;
setIsChartLoading(true);
fetchCountOfIncidentStatusTypeByDays(incidentStatusType, chartFilter)
.then(({ data }) => {
if (!active) return;
setChartData(data.map((item) => ({
timestamp: +item.timestamp,
count: +item.stateId,
})));
})
.catch(() => { if (active) setChartData([]); })
.finally(() => { if (active) setIsChartLoading(false); });
return () => { active = false; };
}, [chartFilter, incidentStatusType]);
Also apply the same guard pattern to sibling widgets with the same unguarded useEffect → setChartData shape:
openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTimeChartWidget/IncidentTimeChartWidget.component.tsx
openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusAreaChartWidget/TestCaseStatusAreaChartWidget.component.tsx
History
This bug was introduced in commit 88c1cf1b476 ("feat: migrate Data quality dashboard to open source (PR #26765)").
Detail Bug Report
https://app.detail.dev/org_3377c26d-da48-4ccd-b83a-22c542f4fe83/bugs/bug_d897e247-f5dc-438c-843b-8566d68c4a86
Introduced in #26765 by @ShaileshParmar11 on Mar 28, 2026
Summary
IncidentTypeAreaChartWidgetrenders the "Open Incidents" / "Resolved Incidents" cards on the Data Quality dashboard, fetching per-day incident counts viafetchCountOfIncidentStatusTypeByDayswhenever the dashboard'schartFilterprop changes.useEffectthat triggersgetCountOfIncidentStatushas noAbortControllerand no ignore-stale-response guard, so whenchartFilterchanges while a previous request is still in flight, the stale response can resolve after the fresh one and overwrite the correct data with data for a filter the user no longer has selected.redirectPathis built by the parent (DqDashboardSectionContent.component.tsx) from the currentdefaultFiltersprop, not from the widget's localchartData, so navigation always lands on the incident manager scoped to the correct date range.Code with Bug
openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTypeAreaChartWidget/IncidentTypeAreaChartWidget.component.tsx:Explanation
chartFilterobject identity and re-triggers the effect.setChartDataapplies whichever response resolves last, even if it corresponds to an older filter.Codebase Inconsistency
The click-through URL is constructed by the parent from current
defaultFilters, not the widget’s potentially stalechartData, so navigation remains correct even when the card display is wrong:openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DqDashboard/DqDashboardSectionContent/DqDashboardSectionContent.component.tsx:Failing Test
Run against the unmodified component (FAIL):
Recommended Fix
Add an effect cleanup “ignore stale resolution” guard (rather than
AbortController, because requests may be coalesced by the batcher):Also apply the same guard pattern to sibling widgets with the same unguarded
useEffect → setChartDatashape:openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTimeChartWidget/IncidentTimeChartWidget.component.tsxopenmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusAreaChartWidget/TestCaseStatusAreaChartWidget.component.tsxHistory
This bug was introduced in commit
88c1cf1b476("feat: migrate Data quality dashboard to open source (PR #26765)").