Skip to content

Issue: add dynamic result classification and issue tracking - #353

Draft
ol-nata wants to merge 21 commits into
ts-factory:mainfrom
ol-nata:feat/issues
Draft

Issue: add dynamic result classification and issue tracking#353
ol-nata wants to merge 21 commits into
ts-factory:mainfrom
ol-nata:feat/issues

Conversation

@ol-nata

@ol-nata ol-nata commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

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 Issue is the underlying cause — a single record shared across projects, so the same bug isn't re-triaged from scratch in every project. An IssueRule is a per-project triage decision on top of it: a category, an expected disposition, 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 — a RuleResult stamp links them. A result stamped expected=True under 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 classify endpoint 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

Issue

The cause identity — global across projects, the dedup point. Carries no classification of its own; category and expected live on the rule.

Fields:

  • title — internal label;
  • description — internal notes, optional;
  • stateopen/closed, Bublik's own view of whether the bug is still active;
  • issue_ext — optional 1:1 link to IssueExt, null for issues with no external tracker reference;
  • created_by/created_at, updated_by/updated_at, closed_by/closed_at — audit fields.

IssueExt

Cache of an external tracker bug. key is set at triage time; status and title are meant to be populated by a future tracker connector and stay null until then.

Fields:

  • key — external bug reference, ref://TRACKER/KEY form, unique;
  • status, title — cached external status/summary;
  • synced_at — last successful sync timestamp;
  • raw — optional raw tracker payload cache.

IssueRule

The 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 of product-defect, test-bug, env, known-issue, flaky, to-investigate;
  • expected — tri-state: true suppresses the result from unexpected counts, false keeps it unexpected, null is 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;
  • audit fields (created_by/created_at, updated_by/updated_at, deactivated_by/deactivated_at).

Module-level helper: default_expected_for(category) suggests an expected value for a category; it's just a default, editable per rule.

RuleResult

The 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;
  • originimport (picked up automatically during import), manual_persistent (classified via an active, future-facing rule), or manual_oneoff (this result only).

Serializers

IssueExtSerializer

IssueExt model serializer. status, title, and synced_at are read-only — they're populated by the tracker connector, not set by users.

IssueSerializer

Issue model serializer, with a write-only bug_key field 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 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.

Methods:

  • 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 — 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, and IssueRuleViewSet have been created, and RunViewSet/ResultViewSet/HistoryViewSet gain new actions and filters.

Note!
All mutating actions on issues and rules require manage_issues permission. The access_token is retrieved from request cookies.

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:

[
    {
        "id": 12,
        "title": "Segfault on teardown",
        "key": "ref://JIRA/FOO-123",
        "category": "product-defect"
    }
]

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:

{
    "title": "Segfault on teardown",
    "description": "Optional notes",
    "bug_key": "ref://JIRA/FOO-123"
}

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_key is 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:

{
    "project": 1,
    "issue": 12,
    "category": "known-issue",
    "test": 45,
    "parameters": {"mode": "release"},
    "verdicts": ["Segmentation fault"],
    "tags": []
}

expected defaults from category if 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 matcher override), and stamps the result.

Request data:

{
    "issue": 12,
    "category": "known-issue",
    "expected": true,
    "scope": "future",
    "matcher": {}
}

issue accepts either an existing issue ID or {title, description?, bug_key?} to create one. scope=future creates an active rule that applies to future imports too; scope=oneoff stamps only this result.

Response data:

{
    "issue_id": 12,
    "rule_id": 34
}

Response status code: 201

Runs

10. GET api/v2/run/<ID>/issues/

Per-issue summary of classified results in the run.

Response data:

[
    {
        "issue_id": 12,
        "title": "Segfault on teardown",
        "state": "open",
        "bug_key": "ref://JIRA/FOO-123",
        "result_count": 3,
        "categories": [
            {"category": "known-issue", "expected": true}
        ]
    }
]

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:

[
    {
        "result_id": 555,
        "name": "testpmd_txonly",
        "path": ["pkg1", "pkg2"],
        "obtained_result": "FAILED",
        "verdicts": ["Segmentation fault"]
    }
]

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:

{
    "stamps_created": 7
}

Response status code: 200

History

GET api/v2/history/

Gains new query parameters for filtering by classification state:

  • categories (string; optional) — comma-separated IssueRule categories, 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 issues field — the list of classifications stamped on it.

{
    ...,
    "issues": [
        {
            "issue_id": 12,
            "issue_title": "Segfault on teardown",
            "issue_state": "open",
            "bug_key": "ref://JIRA/FOO-123",
            "category": "known-issue",
            "expected": true,
            "rule_id": 34,
            "origin": "import"
        }
    ]
}

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) and effective_expected — a boolean, true when the result is suppressed by an expected classification under a still-open issue.

{
    ...,
    "issues": [ ... ],
    "effective_expected": true
}

Needs a visual distinction for results where effective_expected is true (e.g. shown as "known" rather than a plain failure), consistent with how issues is displayed in history.

Deployment

./scripts/deploy --steps migrate_db run_services

ol-nata added 21 commits July 17, 2026 09:28
…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>
@ol-nata ol-nata changed the title Feat/issues Issue: add dynamic result classification and issue tracking Jul 31, 2026
@okt-limonikas

okt-limonikas commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Important Existing Behavior

Multi-value parameters should use ;, matching settings.QUERY_DELIMITER
(bublik/settings.py:373). Date filters should include the complete requested day.

Missing Query Parameters

GET /api/v2/issues/

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

  1. Scope project and category to the same IssueRule. The current chained reverse-relation
    filters (issue/views.py:45-48) can use separate joins and match two different rules.
  2. Centralize the classification state calculation. The suppression rule is
    expected=True and issue.state=open (core/classification.py:15); filtering and serializers
    must use the same definitions.
  3. Apply filters before the existing pagination and test that pagination.count describes the
    filtered set, not only the first 25 records.
  4. Document all parameters and routes in the generated OpenAPI schema.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants