Skip to content

Commit b74dd8a

Browse files
Gustavo Floresalanpeixinho
andauthored
feat: dynamic metrics page (#1920)
* feat: Include page for interactive notification metrics Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi> * feat(metrics): expose new build issues by origin Add a parallel query that lists build issues whose first-ever build incident falls in the selected period, and return them as new_issues_by_origin on GET /api/metrics/. Signed-off-by: Gustavo Flores <gustavo.flores@profusion.mobi> * feat(metrics): expand build regressions with new issue list Add per-origin expandable rows in Build Regressions that show new issue comments, incident counts, and links to issue details. Signed-off-by: Gustavo Flores <gustavo.flores@profusion.mobi> * feat: cache metrics query work --------- Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi> Signed-off-by: Gustavo Flores <gustavo.flores@profusion.mobi> Co-authored-by: Alan Peixinho <alan.peixinho@profusion.mobi>
1 parent 95c8c89 commit b74dd8a

15 files changed

Lines changed: 866 additions & 12 deletions

File tree

backend/kernelCI_app/queries/notifications.py

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.db import connection, connections
66
from pydantic import ValidationError
77

8+
from kernelCI_app.cache import get_query_cache, set_query_cache
89
from kernelCI_app.helpers.database import dict_fetchall
910
from kernelCI_app.helpers.logger import out
1011
from kernelCI_app.queries.tree import get_tree_listing_query
@@ -644,22 +645,32 @@ def get_issues_summary_data(*, checkout_ids: list[str]) -> list[dict]:
644645
return dict_fetchall(cursor=cursor)
645646

646647

647-
def query_fetchone_work(*, query: str, params: dict[str, Any]):
648+
def query_fetchone_work(*, cache_key: str, query: str, params: dict[str, Any]):
649+
rows = get_query_cache(key=cache_key, params=params)
650+
if rows is not None:
651+
return rows
648652
try:
649653
with connections["default"].cursor() as cursor:
650654
cursor.execute(query, params)
651-
return cursor.fetchone()
655+
rows = cursor.fetchone()
652656
finally:
653657
connections["default"].close()
658+
set_query_cache(key=cache_key, params=params, rows=rows)
659+
return rows
654660

655661

656-
def query_fetchall_work(*, query: str, params: dict[str, Any]):
662+
def query_fetchall_work(*, cache_key: str, query: str, params: dict[str, Any]):
663+
rows = get_query_cache(key=cache_key, params=params)
664+
if rows is not None:
665+
return rows
657666
try:
658667
with connections["default"].cursor() as cursor:
659668
cursor.execute(query, params)
660-
return cursor.fetchall()
669+
rows = cursor.fetchall()
661670
finally:
662671
connections["default"].close()
672+
set_query_cache(key=cache_key, params=params, rows=rows)
673+
return rows
663674

664675

665676
def get_metrics_data(
@@ -790,6 +801,42 @@ def get_metrics_data(
790801
WHERE r.ranked <= 3 AND n.total_incidents > 0
791802
"""
792803

804+
new_build_issues_query = """
805+
WITH time_rank AS (
806+
SELECT
807+
_timestamp,
808+
origin,
809+
issue_id,
810+
ROW_NUMBER() OVER (PARTITION BY issue_id ORDER BY _timestamp) AS rn
811+
FROM incidents
812+
WHERE build_id IS NOT NULL
813+
),
814+
new_issues AS (
815+
SELECT issue_id, origin
816+
FROM time_rank
817+
WHERE rn = 1
818+
AND _timestamp BETWEEN
819+
NOW() - INTERVAL %(start_days_ago)s
820+
AND NOW() - INTERVAL %(end_days_ago)s
821+
)
822+
SELECT
823+
inc.origin,
824+
inc.issue_id,
825+
inc.issue_version,
826+
i.comment,
827+
COUNT(inc.*) AS total
828+
FROM incidents inc
829+
JOIN issues i ON inc.issue_id = i.id AND inc.issue_version = i.version
830+
JOIN new_issues ni ON inc.issue_id = ni.issue_id AND inc.origin = ni.origin
831+
WHERE
832+
inc.build_id IS NOT NULL
833+
AND inc._timestamp BETWEEN
834+
NOW() - INTERVAL %(start_days_ago)s
835+
AND NOW() - INTERVAL %(end_days_ago)s
836+
GROUP BY inc.origin, inc.issue_id, inc.issue_version, i.comment
837+
ORDER BY inc.origin, total DESC
838+
"""
839+
793840
lab_summary_query = """
794841
-- get count of tests of each lab and how many builds are related to those tests
795842
SELECT
@@ -806,32 +853,55 @@ def get_metrics_data(
806853
GROUP BY lab
807854
"""
808855

809-
with ThreadPoolExecutor(max_workers=5) as executor:
856+
with ThreadPoolExecutor(max_workers=6) as executor:
810857
total_objects_result = executor.submit(
811-
query_fetchone_work, query=total_objects_query, params=params
858+
query_fetchone_work,
859+
cache_key="metricsTotalObjects",
860+
query=total_objects_query,
861+
params=params,
812862
)
813863
prev_total_objects_result = executor.submit(
814-
query_fetchone_work, query=total_objects_query, params=prev_params
864+
query_fetchone_work,
865+
cache_key="metricsTotalObjects",
866+
query=total_objects_query,
867+
params=prev_params,
815868
)
816869
build_incidents_result = executor.submit(
817-
query_fetchall_work, query=build_incidents_query, params=params
870+
query_fetchall_work,
871+
cache_key="metricsBuildIncidents",
872+
query=build_incidents_query,
873+
params=params,
874+
)
875+
new_build_issues_result = executor.submit(
876+
query_fetchall_work,
877+
cache_key="metricsNewBuildIssues",
878+
query=new_build_issues_query,
879+
params=params,
818880
)
819881
lab_summary_results = executor.submit(
820-
query_fetchall_work, query=lab_summary_query, params=params
882+
query_fetchall_work,
883+
cache_key="metricsLabSummary",
884+
query=lab_summary_query,
885+
params=params,
821886
)
822887
prev_lab_summary_results = executor.submit(
823-
query_fetchall_work, query=lab_summary_query, params=prev_params
888+
query_fetchall_work,
889+
cache_key="metricsLabSummary",
890+
query=lab_summary_query,
891+
params=prev_params,
824892
)
825893

826894
total_objects_result = total_objects_result.result()
827895
prev_total_objects_result = prev_total_objects_result.result()
828896
build_incidents_result = build_incidents_result.result()
897+
new_build_issues_result = new_build_issues_result.result()
829898
lab_summary_results = lab_summary_results.result()
830899
prev_lab_summary_results = prev_lab_summary_results.result()
831900

832901
try:
833902
build_incidents_by_origin: dict[str, BuildIncidentsCount] = {}
834903
top_issues_by_origin: dict[str, dict[tuple[str, int], TopIssue]] = {}
904+
new_issues_by_origin: dict[str, dict[tuple[str, int], TopIssue]] = {}
835905
for row in build_incidents_result:
836906
origin = row[0]
837907
issue_id = row[4]
@@ -851,6 +921,19 @@ def get_metrics_data(
851921
total_incidents=row[7],
852922
)
853923

924+
for row in new_build_issues_result:
925+
origin = row[0]
926+
issue_id = row[1]
927+
issue_version = row[2]
928+
if new_issues_by_origin.get(origin) is None:
929+
new_issues_by_origin[origin] = {}
930+
new_issues_by_origin[origin][(issue_id, issue_version)] = TopIssue(
931+
id=issue_id,
932+
version=issue_version,
933+
comment=row[3],
934+
total_incidents=row[4],
935+
)
936+
854937
data = MetricsReportData(
855938
n_trees=total_objects_result[0],
856939
n_checkouts=total_objects_result[1],
@@ -860,6 +943,7 @@ def get_metrics_data(
860943
n_incidents=total_objects_result[5],
861944
build_incidents_by_origin=build_incidents_by_origin,
862945
top_issues_by_origin=top_issues_by_origin,
946+
new_issues_by_origin=new_issues_by_origin,
863947
lab_maps={
864948
row[0]: LabMetricsData(
865949
builds=row[1],

backend/kernelCI_app/tests/integrationTests/metrics_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"n_incidents",
2222
"build_incidents_by_origin",
2323
"top_issues_by_origin",
24+
"new_issues_by_origin",
2425
"lab_maps",
2526
"prev_n_trees",
2627
"prev_n_checkouts",

backend/kernelCI_app/tests/unitTests/commands/metrics_notifications_test.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ def make_metrics_data(**overrides) -> MetricsReportData:
6262
),
6363
},
6464
},
65+
new_issues_by_origin={
66+
"maestro": {
67+
("issue-new", 1): TopIssue(
68+
id="issue-new",
69+
version=1,
70+
comment="New regression issue",
71+
total_incidents=10,
72+
),
73+
},
74+
},
6575
lab_maps={
6676
"lava-collabora": LabMetricsData(
6777
builds=0, boots=50000, tests=450000, origin="maestro"
@@ -242,6 +252,7 @@ def test_render_receives_fields(
242252
n_incidents=75,
243253
build_incidents_by_origin=mock.ANY,
244254
top_issues_by_origin=mock.ANY,
255+
new_issues_by_origin=mock.ANY,
245256
lab_maps=mock.ANY,
246257
prev_n_trees=100,
247258
prev_n_checkouts=1000,

backend/kernelCI_app/typeModels/metrics.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class MetricsResponse(BaseModel):
3434
n_incidents: int
3535
build_incidents_by_origin: dict[str, BuildIncidentsCount]
3636
top_issues_by_origin: dict[str, list[TopIssue]]
37+
new_issues_by_origin: dict[str, list[TopIssue]]
3738
lab_maps: dict[str, LabMetricsData]
3839
prev_n_trees: int
3940
prev_n_checkouts: int
@@ -55,6 +56,10 @@ def metrics_report_data_to_response(data: MetricsReportData) -> MetricsResponse:
5556
origin: list(issues.values())
5657
for origin, issues in data.top_issues_by_origin.items()
5758
},
59+
new_issues_by_origin={
60+
origin: list(issues.values())
61+
for origin, issues in data.new_issues_by_origin.items()
62+
},
5863
lab_maps=data.lab_maps,
5964
prev_n_trees=data.prev_n_trees,
6065
prev_n_checkouts=data.prev_n_checkouts,

backend/kernelCI_app/typeModels/metrics_notifications.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class MetricsReportData(BaseModel):
3434
build_incidents_by_origin: dict[str, BuildIncidentsCount]
3535
# top_issues = origin -> (issue_id, version) -> TopIssue
3636
top_issues_by_origin: dict[str, dict[tuple[str, int], TopIssue]]
37+
# new_issues = origin -> (issue_id, version) -> TopIssue (first build incident in period)
38+
new_issues_by_origin: dict[str, dict[tuple[str, int], TopIssue]]
3739
lab_maps: dict[str, LabMetricsData]
3840
# Previous interval (for comparison)
3941
prev_n_trees: int

dashboard/src/api/metrics.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
2+
3+
import type { MetricsResponse } from '@/types/metrics';
4+
5+
import { RequestData } from './commonRequest';
6+
7+
type FetchMetricsParams = {
8+
startDaysAgo: number;
9+
endDaysAgo: number;
10+
};
11+
12+
export const fetchMetrics = async ({
13+
startDaysAgo,
14+
endDaysAgo,
15+
}: FetchMetricsParams): Promise<MetricsResponse> => {
16+
const data = await RequestData.get<MetricsResponse>('/api/metrics/', {
17+
params: {
18+
start_days_ago: startDaysAgo,
19+
end_days_ago: endDaysAgo,
20+
},
21+
});
22+
23+
return data;
24+
};
25+
26+
export const useMetrics = ({
27+
intervalInDays,
28+
}: {
29+
intervalInDays: number;
30+
}): UseQueryResult<MetricsResponse> => {
31+
return useQuery({
32+
queryKey: ['metrics', intervalInDays],
33+
queryFn: () =>
34+
fetchMetrics({
35+
startDaysAgo: intervalInDays,
36+
endDaysAgo: 0,
37+
}),
38+
});
39+
};

dashboard/src/components/SideMenu/menuItems.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { JSX } from 'react';
22

3-
import { MdOutlineMonitorHeart } from 'react-icons/md';
3+
import { MdOutlineBarChart, MdOutlineMonitorHeart } from 'react-icons/md';
44
import { RxRadiobutton } from 'react-icons/rx';
55
import { ImTree } from 'react-icons/im';
66
import { HiOutlineDocumentSearch } from 'react-icons/hi';
@@ -30,6 +30,7 @@ export type LinkStringItems = {
3030
const TreeIcon = <ImTree className="size-5" />;
3131
const MonitorHeartIcon = <MdOutlineMonitorHeart className="size-5" />;
3232
const RadioButtonIcon = <RxRadiobutton className="size-5" />;
33+
const MetricsIcon = <MdOutlineBarChart className="size-5" />;
3334
const DocumentSearchIcon = <HiOutlineDocumentSearch />;
3435

3536
export const routeItems: RouteMenuItems[] = [
@@ -51,6 +52,12 @@ export const routeItems: RouteMenuItems[] = [
5152
icon: RadioButtonIcon,
5253
selected: false,
5354
},
55+
{
56+
navigateTo: '/metrics',
57+
idIntl: 'routes.metricsMonitor',
58+
icon: MetricsIcon,
59+
selected: false,
60+
},
5461
];
5562

5663
export const linkItems: LinkMenuItems[] = [

dashboard/src/components/TopBar/TopBar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ const TitleName = ({ basePath }: { basePath: string }): JSX.Element => {
108108
return <FormattedMessage id="routes.testDetails" />;
109109
case 'issue':
110110
return <FormattedMessage id="routes.issueDetails" />;
111+
case 'metrics':
112+
return <FormattedMessage id="routes.metricsMonitor" />;
111113
default:
112114
return <FormattedMessage id="routes.unknown" />;
113115
}

dashboard/src/locales/messages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ export const messages = {
291291
'routes.hardwareNewMonitor': 'Hardware New',
292292
'routes.issueDetails': 'Issue',
293293
'routes.issueMonitor': 'Issues',
294+
'routes.metricsMonitor': 'Metrics',
294295
'routes.testDetails': 'Test',
295296
'routes.treeMonitor': 'Trees',
296297
'routes.unknown': 'Unknown',

0 commit comments

Comments
 (0)