summaryrefslogtreecommitdiffstats
path: root/.github/scripts/analytics
diff options
context:
space:
mode:
authorKirill Rysin <[email protected]>2026-04-22 14:26:25 +0200
committerGitHub <[email protected]>2026-04-22 14:26:25 +0200
commit3998280eede013947cdcf934ff1ebbdaa09b04db (patch)
tree10ced931c519cdcd04aa5041c56e168712229fa3 /.github/scripts/analytics
parent7a7c0968705282c6bd1cad2f36b15b8511fbe238 (diff)
MUTE: Fast unmute (#38430)
Diffstat (limited to '.github/scripts/analytics')
-rw-r--r--.github/scripts/analytics/data_mart_queries/muted_tests_with_issue_and_area.sql39
-rw-r--r--.github/scripts/analytics/data_mart_queries/test_muted_monitor_mart_with_issue.sql47
-rwxr-xr-x.github/scripts/analytics/export_issues_to_ydb.py97
3 files changed, 172 insertions, 11 deletions
diff --git a/.github/scripts/analytics/data_mart_queries/muted_tests_with_issue_and_area.sql b/.github/scripts/analytics/data_mart_queries/muted_tests_with_issue_and_area.sql
index f6cb1651a27..98b4cefd665 100644
--- a/.github/scripts/analytics/data_mart_queries/muted_tests_with_issue_and_area.sql
+++ b/.github/scripts/analytics/data_mart_queries/muted_tests_with_issue_and_area.sql
@@ -1,4 +1,12 @@
-$window_days = 365;
+-- Mart slice: how far back ``tests_monitor.date_window`` is included (not ``mute_config.json``).
+$mart_history_days = 365;
+
+-- Mart filter: branch/build_type slice for this dashboard (not necessarily all CI matrix branches).
+$mart_branch = 'main';
+$mart_build_type = 'relwithdebinfo';
+
+-- Must match ``manual_unmute_ttl_calendar_days`` in ``.github/config/mute_config.json`` (fast-unmute deadline).
+$manual_unmute_ttl_calendar_days = 3;
$normalize = ($raw_area) -> {
$parts = String::SplitToList(Cast($raw_area AS String), '/');
@@ -44,6 +52,18 @@ $gim_latest = (
WHERE g_rnk.rn = 1
);
+$mfu = (
+ SELECT
+ full_name AS full_name,
+ branch AS branch,
+ build_type AS build_type,
+ github_issue_number AS mfu_issue_number,
+ requested_at AS mfu_since,
+ window_days AS mfu_window_days,
+ requested_at + $manual_unmute_ttl_calendar_days * Interval("P1D") AS mfu_expires_at
+ FROM `test_mute/fast_unmute_active`
+);
+
SELECT
tm.state_filtered AS state_filtered,
tm.test_name AS test_name,
@@ -82,7 +102,12 @@ SELECT
gim.github_issue_state AS github_issue_state,
gim.github_issue_created_at AS github_issue_created_at,
gim.area_override AS area_override,
- gim.area_override_since AS area_override_since
+ gim.area_override_since AS area_override_since,
+ CAST(CASE WHEN mfu.full_name IS NOT NULL THEN 1 ELSE 0 END AS Uint8) AS is_manual_fast_unmute,
+ mfu.mfu_since AS manual_fast_unmute_since,
+ mfu.mfu_window_days AS manual_fast_unmute_window_days,
+ mfu.mfu_expires_at AS manual_fast_unmute_expires_at,
+ mfu.mfu_issue_number AS manual_fast_unmute_issue_number
FROM `test_results/analytics/tests_monitor` AS tm
LEFT JOIN $area_fallback AS af
ON Unicode::ToLower(Cast(Coalesce(String::ReplaceAll(tm.owner, 'TEAM:@ydb-platform/', ''), '') AS Utf8)) = af.owner_team
@@ -90,9 +115,13 @@ LEFT JOIN $gim_latest AS gim
ON tm.full_name = gim.full_name
AND tm.branch = gim.branch
AND tm.build_type = gim.build_type
-WHERE tm.date_window >= CurrentUtcDate() - $window_days * Interval("P1D")
- AND tm.branch = 'main'
- AND tm.build_type = 'relwithdebinfo'
+LEFT JOIN $mfu AS mfu
+ ON tm.full_name = mfu.full_name
+ AND tm.branch = mfu.branch
+ AND tm.build_type = mfu.build_type
+WHERE tm.date_window >= CurrentUtcDate() - $mart_history_days * Interval("P1D")
+ AND tm.branch = $mart_branch
+ AND tm.build_type = $mart_build_type
AND tm.is_test_chunk = 0
AND tm.is_muted = 1
AND tm.state != 'Skipped';
diff --git a/.github/scripts/analytics/data_mart_queries/test_muted_monitor_mart_with_issue.sql b/.github/scripts/analytics/data_mart_queries/test_muted_monitor_mart_with_issue.sql
index 382392c2340..0c539268286 100644
--- a/.github/scripts/analytics/data_mart_queries/test_muted_monitor_mart_with_issue.sql
+++ b/.github/scripts/analytics/data_mart_queries/test_muted_monitor_mart_with_issue.sql
@@ -1,6 +1,20 @@
-- GitHub issue fields from github_issue_mapping; analytics area/owner + owner hand-off from tests_monitor.
-- COALESCE fallback: when effective_* columns are NULL (not yet populated), fall back to owner string + area_to_owner_mapping.
+-- Mart slice: last N calendar days of ``tests_monitor`` (not ``mute_config.json``).
+$mart_monitor_date_span_days = 1;
+
+-- Mart branch filter: ``main`` plus release branches matching this prefix pattern.
+$mart_main_branch = 'main';
+$mart_stable_branch_like = 'stable-%';
+
+-- ``resolution`` / ``is_muted_or_skipped``: dashboard SLA thresholds (not ``mute_config.json`` windows).
+$resolution_skipped_days_threshold = 14;
+$resolution_muted_delete_candidate_days = 30;
+
+-- Must match ``manual_unmute_ttl_calendar_days`` in ``.github/config/mute_config.json`` (fast-unmute deadline).
+$manual_unmute_ttl_calendar_days = 3;
+
$normalize = ($raw_area) -> {
$parts = String::SplitToList(Cast($raw_area AS String), '/');
RETURN Cast(
@@ -45,6 +59,18 @@ $gim_latest = (
WHERE g_rnk.rn = 1
);
+$mfu = (
+ SELECT
+ full_name AS full_name,
+ branch AS branch,
+ build_type AS build_type,
+ github_issue_number AS mfu_issue_number,
+ requested_at AS mfu_since,
+ window_days AS mfu_window_days,
+ requested_at + $manual_unmute_ttl_calendar_days * Interval("P1D") AS mfu_expires_at
+ FROM `test_mute/fast_unmute_active`
+);
+
SELECT
tm.state_filtered AS state_filtered,
tm.test_name AS test_name,
@@ -72,8 +98,8 @@ SELECT
tm.state_change_date_filtered AS state_change_date_filtered,
tm.days_in_state_filtered AS days_in_state_filtered,
CAST(CASE
- WHEN (tm.state = 'Skipped' AND tm.days_in_state > 14) THEN 'Skipped'
- WHEN tm.days_in_mute_state > 30 THEN 'MUTED: delete candidate'
+ WHEN (tm.state = 'Skipped' AND tm.days_in_state > $resolution_skipped_days_threshold) THEN 'Skipped'
+ WHEN tm.days_in_mute_state > $resolution_muted_delete_candidate_days THEN 'MUTED: delete candidate'
ELSE 'MUTED: in sla'
END
as String) AS resolution,
@@ -86,7 +112,7 @@ SELECT
tm.effective_owner_team_changed_date AS effective_owner_team_changed_date,
CAST(
CASE
- WHEN tm.is_muted = 1 OR (tm.state = 'Skipped' AND tm.days_in_state > 14) THEN TRUE
+ WHEN tm.is_muted = 1 OR (tm.state = 'Skipped' AND tm.days_in_state > $resolution_skipped_days_threshold) THEN TRUE
ELSE FALSE
END AS Uint8
) AS is_muted_or_skipped,
@@ -95,7 +121,12 @@ SELECT
gim.github_issue_state AS github_issue_state,
gim.github_issue_created_at AS github_issue_created_at,
gim.area_override AS area_override,
- gim.area_override_since AS area_override_since
+ gim.area_override_since AS area_override_since,
+ CAST(CASE WHEN mfu.full_name IS NOT NULL THEN 1 ELSE 0 END AS Uint8) AS is_manual_fast_unmute,
+ mfu.mfu_since AS manual_fast_unmute_since,
+ mfu.mfu_window_days AS manual_fast_unmute_window_days,
+ mfu.mfu_expires_at AS manual_fast_unmute_expires_at,
+ mfu.mfu_issue_number AS manual_fast_unmute_issue_number
FROM `test_results/analytics/tests_monitor` AS tm
LEFT JOIN $area_fallback AS af
ON Unicode::ToLower(Cast(Coalesce(String::ReplaceAll(tm.owner, 'TEAM:@ydb-platform/', ''), '') AS Utf8)) = af.owner_team
@@ -103,6 +134,10 @@ LEFT JOIN $gim_latest AS gim
ON tm.full_name = gim.full_name
AND tm.branch = gim.branch
AND tm.build_type = gim.build_type
-WHERE tm.date_window >= CurrentUtcDate() - 1 * Interval("P1D")
- AND (tm.branch = 'main' OR tm.branch LIKE 'stable-%')
+LEFT JOIN $mfu AS mfu
+ ON tm.full_name = mfu.full_name
+ AND tm.branch = mfu.branch
+ AND tm.build_type = mfu.build_type
+WHERE tm.date_window >= CurrentUtcDate() - $mart_monitor_date_span_days * Interval("P1D")
+ AND (tm.branch = $mart_main_branch OR tm.branch LIKE $mart_stable_branch_like)
AND tm.is_test_chunk = 0;
diff --git a/.github/scripts/analytics/export_issues_to_ydb.py b/.github/scripts/analytics/export_issues_to_ydb.py
index 5d790a99cd1..15d0d4c49d1 100755
--- a/.github/scripts/analytics/export_issues_to_ydb.py
+++ b/.github/scripts/analytics/export_issues_to_ydb.py
@@ -125,6 +125,28 @@ def fetch_single_issue(org_name: str, repo_name: str, issue_number: int) -> Opti
issueType {
name
}
+ timelineItems(last: 20, itemTypes: [CLOSED_EVENT]) {
+ nodes {
+ ... on ClosedEvent {
+ createdAt
+ actor {
+ __typename
+ login
+ }
+ }
+ }
+ }
+ projectItems(first: 30) {
+ nodes {
+ id
+ project {
+ id
+ number
+ title
+ url
+ }
+ }
+ }
}
}
}
@@ -225,6 +247,28 @@ def fetch_repository_issues(org_name: str = ORG_NAME, repo_name: str = REPO_NAME
issueType {
name
}
+ timelineItems(last: 20, itemTypes: [CLOSED_EVENT]) {
+ nodes {
+ ... on ClosedEvent {
+ createdAt
+ actor {
+ __typename
+ login
+ }
+ }
+ }
+ }
+ projectItems(first: 30) {
+ nodes {
+ id
+ project {
+ id
+ number
+ title
+ url
+ }
+ }
+ }
}
pageInfo {
hasNextPage
@@ -415,6 +459,45 @@ def parse_datetime(dt_str: Optional[str]) -> Optional[datetime]:
except (ValueError, TypeError):
return None
+
+def extract_last_close_actor(issue: Dict[str, Any]) -> Dict[str, Any]:
+ """Who closed the issue — same timeline rule as ``mute.fast_unmute_github.fetch_issue_closers``."""
+ login = ''
+ actor_type = ''
+ event_at = None
+ nodes = (issue.get('timelineItems') or {}).get('nodes') or []
+ for event in reversed(nodes):
+ if not event:
+ continue
+ actor = event.get('actor') or {}
+ cand_login = actor.get('login') or ''
+ if cand_login:
+ login = cand_login
+ actor_type = actor.get('__typename') or ''
+ event_at = parse_datetime(event.get('createdAt'))
+ break
+ return {'login': login, 'actor_type': actor_type, 'event_at': event_at}
+
+
+def projects_for_info_json(issue: Dict[str, Any]) -> List[Dict[str, Any]]:
+ """Projects (v2) that contain this issue — id/title from GraphQL ``projectItems``."""
+ out = []
+ for node in (issue.get('projectItems') or {}).get('nodes') or []:
+ proj = node.get('project') or {}
+ pid = proj.get('id')
+ if not pid:
+ continue
+ row = {
+ 'project_id': pid,
+ 'project_number': proj.get('number'),
+ 'title': proj.get('title'),
+ 'url': proj.get('url'),
+ 'project_item_id': node.get('id'),
+ }
+ out.append(row)
+ return out
+
+
# --- branch version helpers ---
def parse_branch(label):
if label == 'main':
@@ -497,6 +580,9 @@ def transform_issues_for_ydb(issues: List[Dict[str, Any]], project_fields: Optio
branch = ';'.join(branch_labels) if branch_labels else None
max_branch = get_max_branch(branch_labels) if branch_labels else None
info = {'branch': branch, 'max_branch': max_branch, 'env': env, 'priority': priority, 'area': area}
+ proj_list = projects_for_info_json(issue)
+ if proj_list:
+ info['projects'] = proj_list
# Issue type: GraphQL issueType.name (Bug/Feature/Task), then project field, then label "bug"
issue_type = (issue.get('issueType') or {}).get('name')
if issue_type is None:
@@ -539,6 +625,17 @@ def transform_issues_for_ydb(issues: List[Dict[str, Any]], project_fields: Optio
created_at = parse_datetime(issue.get('createdAt'))
updated_at = parse_datetime(issue.get('updatedAt'))
closed_at = parse_datetime(issue.get('closedAt'))
+
+ closer = extract_last_close_actor(issue)
+ if closer['login']:
+ info['closed_by_login'] = closer['login']
+ if closer['actor_type']:
+ info['closed_by_typename'] = closer['actor_type']
+ if closer['event_at'] is not None:
+ info['closed_event_at_iso'] = closer['event_at'].isoformat()
+ if closed_at:
+ info['closed_at_iso'] = closed_at.isoformat()
+
now = datetime.now(timezone.utc)
is_in_project = bool(issue_project_fields)