Issue: add dynamic result classification and issue tracking - #353
Issue: add dynamic result classification and issue tracking#353ol-nata wants to merge 21 commits into
Conversation
…eries History query builders already carry many optional filter parameters and took them positionally, making it easy to pass the wrong value to the wrong one. Make them keyword-only, matching the convention already used elsewhere. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Looking up a run matched on ID alone, so passing a result's ID returned it as if it were a run instead of raising 404. Require the lookup to only match rows without an associated run. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Looking up a result matched on ID alone, so passing a run's own ID returned it as if it were a result instead of raising 404. Require the lookup to only match rows with an associated run, mirroring the previous fix for run lookups. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
The tracker segment was matched greedily (`.*`), so a key containing `/` (e.g. `ref://JIRA/PROJ/123`) was parsed as tracker `JIRA/PROJ` instead of `JIRA`. Match non-greedily instead, and combine the find-in-text and parse-one-ref regexes into one. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
`prepare_expected_key()` and `get_expected_results()` each duplicated ref parsing and config lookup. Extract into `resolve_ref` in the new `bublik.core.references` module. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Introduce new models to represent classified issues, their external bug references, per-project triage rules, and the stamps linking failing results to the rule that classified them. This establishes a structured, relational way to record which failures are already known, how each project chooses to triage them, and what evidence ties a specific result to an issue, improving traceability and consistency of the classification workflow. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
IssueSerializer — Issue model serializer: - validate_bug_key() method is used to reject bug key changes on an issue that already has classified results, and to reject a bug key already linked to another issue; - create() method. Redefined so that the issue's external bug reference is synced right after creation; - update() method. Redefined so that the external bug reference is synced whenever bug_key is included in the request; - _sync_bug_key() method is used to link, replace, or clear the issue's external bug reference. IssueRuleSerializer — IssueRule model serializer: - validate() method is used to default expected from category on create, and to reject changes to matcher fields (project, issue, test, parameters, verdicts, tags) on a rule that already has classified results. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Stats, dashboard, and tree caches don't reflect classification changes on their own, and there's no shared way yet to drop just the ones that depend on classification state. Add the set of affected cache keys and a method to drop them for a given set of runs. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Add the matching logic that determines which results a classification rule applies to, based on the test and a subset match on parameters, verdicts, and run tags. Also add the entry point for applying active rules to a run on demand. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
IssueViewSet: - get_queryset() method. Redefined to filter objects by state, category and project query params; - create() method. Redefined to create Issue object and set created_by; - partial_update() method. Redefined to update the object, set updated_by and invalidate classification-affected caches for the issue's runs; - destroy() method. Redefined to invalidate classification-affected caches for the issue's runs before deletion; - close() method is used to set the issue state to closed, deactivate its active rules and invalidate classification-affected caches; - reopen() method is used to set the issue state back to open and invalidate classification-affected caches. IssueRuleViewSet: - get_queryset() method. Redefined to filter objects by project and issue query params; - create() method. Redefined to create IssueRule object and set created_by; - partial_update() method. Redefined to update the object, set updated_by and invalidate classification-affected caches for the rule's runs; - destroy() method. Redefined to invalidate classification-affected caches for the rule's runs before deletion; - deactivate() method is used to set active to False and invalidate classification-affected caches; - activate() method is used to set active to True and invalidate classification-affected caches. All methods that process requests require manage_issues permission. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Provide an endpoint to (re)apply the project's active classification rules to an already-imported run, so rules created or edited afterwards can still be applied without a reimport. Safe to call repeatedly, since it only adds missing stamps. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Define what makes a classification suppressing - an expected disposition under a still-open issue - as a single shared rule, so it can be applied consistently everywhere unexpectedness is computed. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
An expected classification had no effect on run statistics, dashboards, or the run tree - failures stayed counted as unexpected everywhere. Apply the suppression consistently across all of them, so a result classified as expected under a still-open issue is no longer counted as unexpected. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Include each result's classification - the issues it's stamped under, and whether an expected classification suppresses it - in the result rows returned by history and run stats. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Nested serializer errors were flattened into plain strings, losing which sub-field each error belonged to. Recurse into nested error values instead, so they stay structured at any depth. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Provide an endpoint for manually classifying a single result: reuse or create an issue, capture a matching rule from the result's own parameters (or an explicit override), and stamp the result. The rule can be created active, applying to future imports too, or as a one-off covering only this result. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Apply the project's active classification rules right after import tags the run, wrapped so a rule-matching error can't fail the whole import. Newly imported runs now get classified automatically instead of requiring a separate manual step. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Add filters to the history view for slicing results by classification state: by category, by a specific issue, by whether a result has been classified at all, or by whether a failure is still waiting for triage. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Provide an endpoint summarizing, per run, which issues its failures are classified under, with distinct result counts and the categories and expected-status seen for each. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Provide an endpoint returning the results in a run classified under a given issue, along with their package path, obtained result, and verdicts, to drill down from the per-issue summary into concrete failures. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Provide an endpoint for searching issues by title or bug key, or listing the most recently used ones when no search term is given, optionally scoped to a project, to populate the issue picker when classifying a result. Signed-off-by: Natalia Rybchenko <natalia.rybchenko@oktetlabs.ru>
Important Existing BehaviorMulti-value parameters should use Missing Query Parameters
|
| Parameter | Values | Why needed |
|---|---|---|
project |
Project ID | Limit issues to a project |
search |
title, description, issue key, #id |
Find a specific issue |
state |
open;closed |
Filter active and closed issues together or separately |
category |
Six IssueCategory values |
Find issues by classification |
rules |
enforced, dormant, deactivated, unruled |
Filter by rule status |
created_after/before |
Date | Date-range filtering |
updated_after/before |
Date | Find recently changed issues |
ordering |
created_at, updated_at, title, state; optional - |
Stable server-side sorting |
project and state already exist (issue/views.py:40-49); state should become delimiter-aware.
category is stored on IssueRule, not Issue (data/models/issue.py:37-44). Both category
and rules must be evaluated against rules belonging to the requested project.
GET /api/v2/issue_rules/
| Parameter | Values | Why needed |
|---|---|---|
search |
Test name, issue title, issue key | Find a rule without knowing its ID |
category |
Six IssueCategory values |
Filter rules by classification |
active |
true;false |
Show active, inactive, or all rules |
expected |
expected, unexpected, none |
Filter the nullable rule disposition |
created_after/before |
Date | Date-range filtering |
ordering |
created_at, category, active, test_name, issue_title; optional - |
Server-side sorting |
project and issue already exist. IssueRule.expected is nullable
(data/models/issue.py:176-179), therefore expected must map to:
expected -> expected=True
unexpected -> expected=False
none -> expected__isnull=True
Use select_related('test', 'issue', 'issue__issue_ext') to avoid an N+1 query when serializing
test and issue fields (data/serializers/issue.py:99-101).
Required Missing Read Route
GET /api/v2/issues/facets/
Return counts for state, category, and rules over the complete filtered set, not only the
current page.
Why needed: page-local counts become misleading after server-side pagination. When calculating
one facet, ignore only that facet's filter and keep the other filters.
If the optional consolidation is accepted, these existing nested routes can later be deprecated:
/api/v2/runs/{run_id}/issues/
/api/v2/runs/{run_id}/issues/{issue_id}/results/
This is a URL design choice, not an additional functional requirement.
Missing Bulk Routes
Consider removing single id actions with bulk maybe dropping bulik_ and just
accepting multiple ids
| Route | Why needed |
|---|---|
POST /api/v2/issues/bulk_close/ |
Close multiple issues in one request |
POST /api/v2/issues/bulk_reopen/ |
Reopen multiple issues in one request |
POST /api/v2/issue_rules/bulk_activate/ |
Activate multiple rules in one request |
POST /api/v2/issue_rules/bulk_deactivate/ |
Deactivate multiple rules in one request |
Request:
{"ids": [1, 2, 3]}Response:
{"requested": 3, "updated": [1, 3], "unchanged": [2]}Response Changes
Add these fields to the /issues/ response:
bug_key, bug_url, categories, rule_count, active_rule_count, rules_state
Why needed: the API should return the issue's rule summary instead of requiring consumers to
load and join all issue rules themselves.
With the suggested run filter, return run-specific values under run_context; without it, return
the project-wide issue values. This keeps one response shape while making the scope explicit.
Required Backend Fixes
- Scope
projectandcategoryto the sameIssueRule. The current chained reverse-relation
filters (issue/views.py:45-48) can use separate joins and match two different rules. - Centralize the classification state calculation. The suppression rule is
expected=Trueandissue.state=open(core/classification.py:15); filtering and serializers
must use the same definitions. - Apply filters before the existing pagination and test that
pagination.countdescribes the
filtered set, not only the first 25 records. - Document all parameters and routes in the generated OpenAPI schema.
Info
This PR adds dynamic result classification: the ability to mark a failing result as a known issue, so that the same failure is recognized automatically in future runs and, if desired, no longer counted as unexpected — without touching the existing static TRC mechanism.
The classification is split in two layers. An
Issueis the underlying cause — a single record shared across projects, so the same bug isn't re-triaged from scratch in every project. AnIssueRuleis a per-project triage decision on top of it: a category, anexpecteddisposition, and a matcher (test + a subset of parameters/verdicts/tags) that says which results the rule recognizes. When an active rule's matcher fits a result — on import or on demand — aRuleResultstamp links them. A result stampedexpected=Trueunder a still-open issue is suppressed from unexpected/NOK counts everywhere they're computed (run stats, dashboards, the run tree).Results can be classified two ways: automatically, by applying the project's active rules right after import (best-effort, doesn't fail the import); or manually, via a
classifyendpoint that resolves/creates an issue and captures a rule from the result's own data. Rules created or edited later can also be applied to an already-imported run on demand.Overview of changes
Models
IssueThe cause identity — global across projects, the dedup point. Carries no classification of its own; category and
expectedlive on the rule.Fields:
title— internal label;description— internal notes, optional;state—open/closed, Bublik's own view of whether the bug is still active;issue_ext— optional 1:1 link toIssueExt, null for issues with no external tracker reference;created_by/created_at,updated_by/updated_at,closed_by/closed_at— audit fields.IssueExtCache of an external tracker bug.
keyis set at triage time;statusandtitleare meant to be populated by a future tracker connector and stay null until then.Fields:
key— external bug reference,ref://TRACKER/KEYform, unique;status,title— cached external status/summary;synced_at— last successful sync timestamp;raw— optional raw tracker payload cache.IssueRuleThe per-project triage decision: what an issue means here, and how to recognize it automatically.
Fields:
project,issue,test— what this rule applies to;category— one ofproduct-defect,test-bug,env,known-issue,flaky,to-investigate;expected— tri-state:truesuppresses the result from unexpected counts,falsekeeps it unexpected,nullis a marker only;active— whether the rule applies to future imports;parameters,verdicts,tags— the captured matcher: subsets that a result's own parameters/verdicts/tags must contain to match; empty means that part of the matcher is ignored;created_by/created_at,updated_by/updated_at,deactivated_by/deactivated_at).Module-level helper:
default_expected_for(category)suggests anexpectedvalue for a category; it's just a default, editable per rule.RuleResultThe stamp connecting a specific result to the rule that classified it, unique per
(result, issue_rule)pair.Fields:
result,issue_rule— the linked result and rule;origin—import(picked up automatically during import),manual_persistent(classified via an active, future-facing rule), ormanual_oneoff(this result only).Serializers
IssueExtSerializerIssueExtmodel serializer.status,title, andsynced_atare read-only — they're populated by the tracker connector, not set by users.IssueSerializerIssuemodel serializer, with a write-onlybug_keyfield for linking to an external bug.Methods:
validate_bug_key()method is used to reject bug key changes on an issue that already has classified results, and to reject a bug key already linked to another issue;create()method. Redefined so the external bug reference is synced right after creation;update()method. Redefined so the external bug reference is synced wheneverbug_keyis included in the request;_sync_bug_key()method is used to link, replace, or clear the issue's external bug reference.IssueRuleSerializerIssueRulemodel serializer.Methods:
validate()method is used to defaultexpectedfromcategoryon create, and to reject changes to matcher fields (project,issue,test,parameters,verdicts,tags) on a rule that already has classified results — a live matcher can't be edited without silently changing what past stamps mean, so a new rule should be created instead.API
IssuePickerViewSet,IssueViewSet, andIssueRuleViewSethave been created, andRunViewSet/ResultViewSet/HistoryViewSetgain new actions and filters.Issues
1.
GET /bublik/api/v2/issues/picker/Search issues by title or bug key, or list the most recently used ones, for the issue picker shown when classifying a result.
Query parameters:
search(string; optional) — matched against title and bug key; without it, the most recently used issues are returned instead;project(integer; optional) — scope results to a project.Response data:
Response status code: 200
2.
api/v2/issues/Allows you to list and create issues.
Allowed methods: GET, POST
Optional GET filters:
state,category,project.POST request data:
Response status code: 201
3.
api/v2/issues/<ID>/Allows you to retrieve, update, or delete an issue.
Allowed methods: GET, PATCH, DELETE
PATCH accepts the same fields as POST, partially. Changing
bug_keyis rejected once the issue has classified results.Response status code: 200 (GET/PATCH), 204 (DELETE)
4.
api/v2/issues/<ID>/close/Closes the issue and deactivates its active rules.
Allowed methods: POST
Response status code: 200
5.
api/v2/issues/<ID>/reopen/Reopens a closed issue.
Allowed methods: POST
Response status code: 200
Issue rules
6.
api/v2/issue_rules/Allows you to list and create classification rules.
Allowed methods: GET, POST
Optional GET filters:
project,issue.POST request data:
expecteddefaults fromcategoryif not provided.Response status code: 201
7.
api/v2/issue_rules/<ID>/Allows you to retrieve, update, or delete a rule.
Allowed methods: GET, PATCH, DELETE
Editing matcher fields (
project,issue,test,parameters,verdicts,tags) is rejected once the rule has classified results.Response status code: 200 (GET/PATCH), 204 (DELETE)
8.
api/v2/issue_rules/<ID>/activate/,api/v2/issue_rules/<ID>/deactivate/Allowed methods: POST
Response status code: 200
Results
9.
POST api/v2/result/<ID>/classify/Classifies a single result: resolves or creates an issue, captures a matching rule (from the result's own parameters/verdicts/tags, or an explicit
matcheroverride), and stamps the result.Request data:
issueaccepts either an existing issue ID or{title, description?, bug_key?}to create one.scope=futurecreates an active rule that applies to future imports too;scope=oneoffstamps only this result.Response data:
Response status code: 201
Runs
10.
GET api/v2/run/<ID>/issues/Per-issue summary of classified results in the run.
Response data:
Response status code: 200
11.
GET api/v2/run/<ID>/issues/<issue_id>/results/Results in the run classified under a given issue, for drilling down from the summary above.
Response data:
Response status code: 200
12.
POST api/v2/run/<ID>/apply_rules/Applies the project's active rules to an already-imported run. Only adds missing stamps, never removes existing ones — safe to call repeatedly.
Response data:
Response status code: 200
History
GET api/v2/history/Gains new query parameters for filtering by classification state:
categories(string; optional) — comma-separatedIssueRulecategories, e.g.known-issue,flaky;issue(string; optional) — comma-separated issue IDs;explained(true; optional) — keep only results with at least one classification;untriaged(true; optional) — keep only failed results with no classification at all.UI Requirements
The following existing endpoints now return classification data that needs to be displayed; no UI exists for it yet.
GET /bublik/api/v2/history/Each result in the response now includes an
issuesfield — the list of classifications stamped on it.Needs a way to show, per result, which issue(s) it's classified under (e.g. a badge with title/category), ideally linking through to the issue.
GET /bublik/api/v2/result/Each result in the response now includes
issues(same shape as above) andeffective_expected— a boolean, true when the result is suppressed by an expected classification under a still-open issue.Needs a visual distinction for results where
effective_expectedis true (e.g. shown as "known" rather than a plain failure), consistent with howissuesis displayed in history.Deployment