Skip to content

Commit a1ce6a5

Browse files
adrianviquezandrewshie-sentry
authored andcommitted
Adrian/feat add test analytics infinite hook (#93198)
This PR adds an infiniteQuery hook to fetch test analytics data from Codecov's backend via Sentry. Notes - Adds the `useInfiniteTestResults` hook. This is a preliminary version of the hook and minimally supports query parameters or sorting. - Header css styling in SortableHeader.tsx. - Removes fake test data from test.tsx page and moves CodecovProvider to the Wrapper parent component to allow context data to be used by this page.
1 parent 7c19dec commit a1ce6a5

File tree

5 files changed

+170
-56
lines changed

5 files changed

+170
-56
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {useMemo} from 'react';
2+
3+
import type {ApiResult} from 'sentry/api';
4+
import {useCodecovContext} from 'sentry/components/codecov/context/codecovContext';
5+
import {
6+
fetchDataQuery,
7+
type InfiniteData,
8+
type QueryKeyEndpointOptions,
9+
useInfiniteQuery,
10+
} from 'sentry/utils/queryClient';
11+
12+
export type TestResultItem = {
13+
avgDuration: number;
14+
commitsFailed: number;
15+
failureRate: number;
16+
flakeRate: number;
17+
lastDuration: number;
18+
name: string;
19+
totalFailCount: number;
20+
totalFlakyFailCount: number;
21+
totalPassCount: number;
22+
totalSkipCount: number;
23+
updatedAt: string;
24+
};
25+
26+
interface TestResults {
27+
pageInfo: {
28+
endCursor: string;
29+
hasNextPage: boolean;
30+
};
31+
results: TestResultItem[];
32+
totalCount: number;
33+
}
34+
35+
type QueryKey = [url: string, endpointOptions: QueryKeyEndpointOptions];
36+
37+
export function useInfiniteTestResults() {
38+
const {integratedOrg, repository} = useCodecovContext();
39+
40+
const {data, ...rest} = useInfiniteQuery<
41+
ApiResult<TestResults>,
42+
Error,
43+
InfiniteData<ApiResult<TestResults>>,
44+
QueryKey
45+
>({
46+
// TODO: this query key should have branch and codecovPeriod so the request updates when these change
47+
queryKey: [
48+
`/prevent/owner/${integratedOrg}/repository/${repository}/test-results/`,
49+
{},
50+
],
51+
queryFn: async ({
52+
queryKey: [url, endpointOptions],
53+
client,
54+
signal,
55+
meta,
56+
}): Promise<ApiResult<TestResults>> => {
57+
// console.log('asdfasdf', client, signal, meta);
58+
const result = await fetchDataQuery({
59+
queryKey: [
60+
url,
61+
{
62+
...endpointOptions,
63+
// TODO: expand when query params are known
64+
query: {},
65+
},
66+
],
67+
client,
68+
signal,
69+
meta,
70+
});
71+
72+
return result as ApiResult<TestResults>;
73+
},
74+
getNextPageParam: ([lastPage]) => {
75+
return lastPage.pageInfo?.hasNextPage ? lastPage.pageInfo.endCursor : undefined;
76+
},
77+
initialPageParam: null,
78+
});
79+
80+
const memoizedData = useMemo(
81+
() =>
82+
data?.pages.flatMap(([pageData]) =>
83+
pageData.results.map(
84+
({
85+
name,
86+
avgDuration,
87+
updatedAt,
88+
totalFailCount,
89+
totalPassCount,
90+
totalFlakyFailCount,
91+
totalSkipCount,
92+
...other
93+
}) => {
94+
const isBrokenTest =
95+
totalFailCount === totalPassCount + totalFlakyFailCount + totalSkipCount;
96+
return {
97+
...other,
98+
testName: name,
99+
averageDurationMs: avgDuration,
100+
lastRun: updatedAt,
101+
isBrokenTest,
102+
};
103+
}
104+
)
105+
) ?? [],
106+
[data]
107+
);
108+
109+
return {
110+
data: memoizedData,
111+
...rest,
112+
};
113+
}

static/app/views/codecov/tests/testAnalyticsTable/sortableHeader.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const HeaderCell = styled('div')<{alignment: string}>`
9797
gap: ${space(1)};
9898
width: 100%;
9999
justify-content: ${p => (p.alignment === 'left' ? 'flex-start' : 'flex-end')};
100+
font-weight: ${p => p.theme.fontWeightBold};
100101
`;
101102

102103
const StyledLink = styled(Link)`

static/app/views/codecov/tests/tests.spec.tsx

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,51 @@
11
import {render, screen} from 'sentry-test/reactTestingLibrary';
22

3+
import CodecovQueryParamsProvider from 'sentry/components/codecov/container/codecovParamsProvider';
34
import TestsPage from 'sentry/views/codecov/tests/tests';
45

6+
const mockTestResultsData = [
7+
{
8+
name: 'tests.symbolicator.test_unreal_full.SymbolicatorUnrealIntegrationTest::test_unreal_crash_with_attachments',
9+
avgDuration: 4,
10+
flakeRate: 0.4,
11+
commitsFailed: 1,
12+
lastRun: '2025-04-17T22:26:19.486793+00:00',
13+
totalFailCount: 1,
14+
totalFlakyFailCount: 2,
15+
totalPassCount: 0,
16+
totalSkipCount: 3,
17+
updatedAt: '2025-04-17T22:26:19.486793+00:00',
18+
},
19+
];
20+
21+
const mockApiCall = () =>
22+
MockApiClient.addMockResponse({
23+
url: `/prevent/owner/some-org-name/repository/some-repository/test-results/`,
24+
method: 'GET',
25+
body: {data: mockTestResultsData},
26+
});
27+
528
describe('CoveragePageWrapper', () => {
629
describe('when the wrapper is used', () => {
30+
mockApiCall();
731
it('renders the passed children', async () => {
8-
render(<TestsPage />, {
9-
initialRouterConfig: {
10-
location: {
11-
pathname: '/codecov/tests',
12-
query: {codecovPeriod: '7d'},
32+
render(
33+
<CodecovQueryParamsProvider>
34+
<TestsPage />
35+
</CodecovQueryParamsProvider>,
36+
{
37+
initialRouterConfig: {
38+
location: {
39+
pathname: '/codecov/tests',
40+
query: {
41+
codecovPeriod: '7d',
42+
integratedOrg: 'some-org-name',
43+
repository: 'some-repository',
44+
},
45+
},
1346
},
14-
},
15-
});
47+
}
48+
);
1649

1750
const testsAnalytics = await screen.findByText('Test Analytics');
1851
expect(testsAnalytics).toBeInTheDocument();

static/app/views/codecov/tests/tests.tsx

Lines changed: 10 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,38 @@
11
import styled from '@emotion/styled';
22

3-
import CodecovQueryParamsProvider from 'sentry/components/codecov/container/codecovParamsProvider';
43
import {DatePicker} from 'sentry/components/codecov/datePicker/datePicker';
54
import {IntegratedOrgSelector} from 'sentry/components/codecov/integratedOrgSelector/integratedOrgSelector';
65
import {RepoPicker} from 'sentry/components/codecov/repoPicker/repoPicker';
76
import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
87
import {space} from 'sentry/styles/space';
98
import {decodeSorts} from 'sentry/utils/queryString';
109
import {useLocation} from 'sentry/utils/useLocation';
10+
import {useInfiniteTestResults} from 'sentry/views/codecov/tests/queries/useGetTestResults';
1111
import {DEFAULT_SORT} from 'sentry/views/codecov/tests/settings';
1212
import {Summaries} from 'sentry/views/codecov/tests/summaries/summaries';
1313
import type {ValidSort} from 'sentry/views/codecov/tests/testAnalyticsTable/testAnalyticsTable';
1414
import TestAnalyticsTable, {
1515
isAValidSort,
1616
} from 'sentry/views/codecov/tests/testAnalyticsTable/testAnalyticsTable';
1717

18-
// TODO: Sorting will only work once this is connected to the API
19-
const fakeApiResponse = {
20-
data: [
21-
{
22-
testName:
23-
'tests.symbolicator.test_unreal_full.SymbolicatorUnrealIntegrationTest::test_unreal_crash_with_attachments',
24-
averageDurationMs: 4,
25-
flakeRate: 0.4,
26-
commitsFailed: 1,
27-
lastRun: '2025-04-17T22:26:19.486793+00:00',
28-
isBrokenTest: false,
29-
},
30-
{
31-
testName:
32-
'graphql_api/tests/test_owner.py::TestOwnerType::test_fetch_current_user_is_not_okta_authenticated',
33-
averageDurationMs: 4370,
34-
flakeRate: 0,
35-
commitsFailed: 5,
36-
lastRun: '2025-04-16T22:26:19.486793+00:00',
37-
isBrokenTest: true,
38-
},
39-
{
40-
testName: 'graphql_api/tests/test_owner.py',
41-
averageDurationMs: 10032,
42-
flakeRate: 1,
43-
commitsFailed: 3,
44-
lastRun: '2025-02-16T22:26:19.486793+00:00',
45-
isBrokenTest: false,
46-
},
47-
],
48-
isLoading: false,
49-
isError: false,
50-
};
51-
5218
export default function TestsPage() {
5319
const location = useLocation();
54-
5520
const sorts: [ValidSort] = [
5621
decodeSorts(location.query?.sort).find(isAValidSort) ?? DEFAULT_SORT,
5722
];
23+
const response = useInfiniteTestResults();
5824

5925
return (
6026
<LayoutGap>
6127
<p>Test Analytics</p>
62-
<CodecovQueryParamsProvider>
63-
<PageFilterBar condensed>
64-
<IntegratedOrgSelector />
65-
<RepoPicker />
66-
<DatePicker />
67-
</PageFilterBar>
68-
{/* TODO: Conditionally show these if the branch we're in is the main branch */}
69-
<Summaries />
70-
<TestAnalyticsTable response={fakeApiResponse} sort={sorts[0]} />
71-
</CodecovQueryParamsProvider>
28+
<PageFilterBar condensed>
29+
<IntegratedOrgSelector />
30+
<RepoPicker />
31+
<DatePicker />
32+
</PageFilterBar>
33+
{/* TODO: Conditionally show these if the branch we're in is the main branch */}
34+
<Summaries />
35+
<TestAnalyticsTable response={response} sort={sorts[0]} />
7236
</LayoutGap>
7337
);
7438
}

static/app/views/codecov/tests/testsWrapper.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Outlet} from 'react-router-dom';
22
import styled from '@emotion/styled';
33

4+
import CodecovQueryParamsProvider from 'sentry/components/codecov/container/codecovParamsProvider';
45
import {FeatureBadge} from 'sentry/components/core/badge/featureBadge';
56
import * as Layout from 'sentry/components/layouts/thirds';
67
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
@@ -23,9 +24,11 @@ export default function TestAnalyticsPageWrapper() {
2324
</Layout.HeaderContent>
2425
</Layout.Header>
2526
<Layout.Body>
26-
<Layout.Main fullWidth>
27-
<Outlet />
28-
</Layout.Main>
27+
<CodecovQueryParamsProvider>
28+
<Layout.Main fullWidth>
29+
<Outlet />
30+
</Layout.Main>
31+
</CodecovQueryParamsProvider>
2932
</Layout.Body>
3033
</SentryDocumentTitle>
3134
);

0 commit comments

Comments
 (0)