diff --git a/tools/ci/play_vitals.py b/tools/ci/play_vitals.py index c2ed2808..41d1284e 100644 --- a/tools/ci/play_vitals.py +++ b/tools/ci/play_vitals.py @@ -237,7 +237,13 @@ def cmd_issues(token, args): print(f" {issue.get('cause')}") print(f" id {issue.get('name')}") reports = search_reports( - token, issue.get("name", ""), start, end, tz, limit=args.reports + token, + issue.get("name", ""), + start, + end, + tz, + limit=args.reports, + version_code=args.version_code, ) for line in report_lines(reports, traces=args.traces): print(line) @@ -278,18 +284,22 @@ def report_lines(reports, traces): return lines -def search_reports(token, issue_name, start, end, tz, limit): +def search_reports(token, issue_name, start, end, tz, limit, version_code=None): """Returns the reports behind one cluster, each with its device and versions. The issue carries sample report names, but only the names, and they are not a resource path any GET accepts. errorReports:search filtered on the issue id is the - route that works. + route that works. A cluster holds several builds, so the filter must repeat the + version, or the printed sample can come from any of them. """ issue_id = issue_name.rsplit("/", 1)[-1] + terms = [f"errorIssueId = {issue_id}"] + if version_code: + terms.append(f"versionCode = {version_code}") params = flatten( { "interval": {"startTime": datetime_at(start, tz), "endTime": datetime_at(end, tz)}, - "filter": f"errorIssueId = {issue_id}", + "filter": " AND ".join(terms), "pageSize": limit, } ) diff --git a/tools/ci/play_vitals_test.py b/tools/ci/play_vitals_test.py index d39fb85a..e4be1b83 100644 --- a/tools/ci/play_vitals_test.py +++ b/tools/ci/play_vitals_test.py @@ -216,6 +216,20 @@ def test_search_reports_sends_a_dotted_interval_and_filter(self): self.assertEqual(seen["params"]["filter"], ["errorIssueId = abc123"]) self.assertEqual(seen["params"]["pageSize"], ["1"]) + def test_a_version_filter_reaches_the_report_search(self): + seen = self.captured_params( + lambda: pv.search_reports( + "t", + "apps/com.httrack.android/abc123", + datetime.date(2026, 10, 2), + datetime.date(2026, 10, 30), + "UTC", + 1, + version_code=107, + ) + ) + self.assertEqual(seen["params"]["filter"], ["errorIssueId = abc123 AND versionCode = 107"]) + def test_the_issue_id_is_taken_from_the_last_path_segment(self): seen = self.captured_params( lambda: pv.search_reports( @@ -361,7 +375,7 @@ def test_probe_takes_no_options(self): class Issues(unittest.TestCase): """cmd_issues must ask the API for as many reports as it was told to read.""" - def run_issues(self, wanted, traces): + def run_issues(self, wanted, traces, version_code=None): pool = [dict(FULL_REPORT, reportText=f"trace-{i}") for i in range(wanted + 2)] asked = {} @@ -375,7 +389,12 @@ def fake_call(_token, url, method="GET", body=None, params=None): return {"errorReports": pool[: int(params["pageSize"][0])]} args = argparse.Namespace( - kind="crash", days=28, limit=15, reports=wanted, traces=traces, version_code=None + kind="crash", + days=28, + limit=15, + reports=wanted, + traces=traces, + version_code=version_code, ) out = io.StringIO() with mock.patch("play_vitals.call", side_effect=fake_call): @@ -387,6 +406,16 @@ def test_the_report_search_asks_for_the_number_of_reports_requested(self): _, asked = self.run_issues(wanted=3, traces=1) self.assertEqual(asked["reports"]["pageSize"], ["3"]) + def test_one_version_restricts_the_reports_as_well_as_the_clusters(self): + _, asked = self.run_issues(wanted=3, traces=1, version_code=107) + self.assertIn("versionCode = 107", asked["issues"]["filter"][0]) + self.assertIn("versionCode = 107", asked["reports"]["filter"][0]) + + def test_without_one_neither_search_names_a_version(self): + _, asked = self.run_issues(wanted=3, traces=1) + self.assertNotIn("versionCode", asked["issues"]["filter"][0]) + self.assertNotIn("versionCode", asked["reports"]["filter"][0]) + def test_the_cluster_search_never_asks_for_more_than_one_sample(self): for wanted in (1, 3, 25): _, asked = self.run_issues(wanted=wanted, traces=1)