Skip to content

fix(alerts): scope thread events by their parent entity instead of bypassing filters - #30571

Open
manerow wants to merge 3 commits into
mainfrom
fix/alert-thread-scoping-filters
Open

fix(alerts): scope thread events by their parent entity instead of bypassing filters#30571
manerow wants to merge 3 commits into
mainfrom
fix/alert-thread-scoping-filters

Conversation

@manerow

@manerow manerow commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Fixes #30555

Problem

An alert scoped to one entity receives conversations about every entity of that type. Reproduced on
a clean main deploy: an alert with resource: glossaryTerm, filterByEventType: [threadCreated, postCreated]
and filterByFqn: [<term T2>] received both events for a conversation started on T1.

threadCreated | thread about: <#E::glossaryTerm::<T1 FQN>::description> | entityRef: <T1 FQN>
postCreated   | thread about: <#E::glossaryTerm::<T1 FQN>::description> | entityRef: <T1 FQN>

The same holds for Owner, Domain, Entity Id and Source: the filter is stored, shown in the UI, and
has no effect.

Root cause

A thread change event carries entityType = THREAD and a Thread payload rather than the entity the
thread is about, so five matchers in AlertsRuleEvaluator short-circuited:

// Filter does not apply to Thread Change Events
if (changeEvent.getEntityType().equals(THREAD)) {
  return true;
}

Returning true from a filter that cannot be evaluated means "deliver", not "does not match". That
was unreachable for entity resources until #28122 correctly routed thread events to the alert of the
entity the thread is about, at which point glossaryTerm, table and friends, which do offer these
filters, started hitting the bypass.

Fix

The event already carries the answer: Thread.entityRef points at the parent entity. Each bypass now
evaluates the same question against that reference.

Matcher Behaviour for a THREAD event
matchAnyEntityFqn matchesFqnOrDescendant(thread.entityRef.fullyQualifiedName, ...), so ancestor scoping from #28833 keeps working
matchAnyEntityId compares thread.entityRef.id
matchAnySource compares thread.entityRef.type
matchAnyOwnerName resolves the parent with owners and reuses the existing matchOwners(...)
matchAnyDomain resolves the parent with domains through a new shared matchDomains(...), also used by the entity path

Owner and domain resolve the parent through the existing Entity.getEntityOrNull(ref, fields, NON_DELETED),
so a null reference, a deleted parent or an unknown entity type yield false instead of throwing. That
matters here: an escaping exception in a matcher aborts the whole change-event batch, the failure mode
fixed in #28304.

Matchers that genuinely cannot apply to a thread (test result, pipeline state, ingestion state) already
return false since #29112 and are untouched.

Also drops the dead announcement entry from AlertUtil.THREAD_TYPE_RESOURCES. Announcement has been a
first-class entity since #25894, its events carry entityType = announcement and take the generic path,
and FeedResource.rejectLegacyAnnouncementAccess blocks the legacy door. task stays until the last
legacy thread-task writer is migrated (#30559).

Behaviour change worth noting in release notes

Alerts that were receiving every thread event because their scoping filter was ignored will now receive
only what they scoped. No migration is required: no schema change, and the filter vocabulary is identical
between 1.13 and main (same eleven filter function names, same per-resource supportedFilters), so
every stored rule keeps parsing and evaluating. Only the meaning for THREAD events changes.

Tests

  • AlertsRuleEvaluatorThreadScopeTest (new, 9 cases): FQN exact / ancestor / sibling-prefix / non-match, entity id, source, and a thread with no entityRef where every scoping filter must return false.
  • AlertsRuleEvaluatorResourceIT (4 new cases): owner and domain against real entities, FQN, and an unresolvable parent. These fail against the pre-fix build and pass after it.
  • AlertUtilTest: the announcement assertions now cover the post-redesign behaviour, plus a case proving an announcement resource still matches real entityType=announcement events.

Verified end to end on a local deploy

Alert Scope Conversations on T1 and T2 Result
unscoped event types only both both delivered, #28122 intact
scoped by FQN to T2 filterByFqn: [T2] both only T2 delivered
scoped by owner filterByOwnerName: [admin], T1 is admin-owned both only T1 delivered
observability, no trigger, no filter - conversation on a table delivered
observability, no trigger, FQN filter filterByFqn: [nonexistent] conversation on a table nothing delivered

The last two cover an edge case worth knowing: the observability trigger section is a form list with no
minimum, so an observability alert can be saved with zero triggers, and the #29112 guard only rejects
thread events when actions is non-empty. Such an alert does receive thread events, and now honours its
filters too.

Also driven through the UI: building the alert in Settings > Notifications > Alerts, commenting on both
glossary terms, and confirming the alert's Recent Events tab shows Total Events: 1 for the in-scope
comment only.

Greptile Summary

Thread-event alerts now evaluate entity, owner, domain, FQN, ID, and source filters against the referenced parent entity.

  • Resolves owner and domain data from the thread subject while treating absent or unresolvable references as non-matches.
  • Removes legacy announcement threads from thread-resource routing while preserving first-class announcement events.
  • Adds unit and integration coverage for thread scoping, malformed IDs, missing references, owner/domain resolution, and announcement routing.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/events/subscription/AlertsRuleEvaluator.java Replaces unconditional thread-filter matches with parent-reference matching and shared null-safe owner, domain, FQN, ID, and type helpers.
openmetadata-service/src/main/java/org/openmetadata/service/events/subscription/AlertUtil.java Removes obsolete announcement threads from legacy thread-resource routing.
openmetadata-service/src/test/java/org/openmetadata/service/events/subscription/AlertsRuleEvaluatorThreadScopeTest.java Adds focused coverage for thread subject scoping, ancestor FQNs, malformed IDs, and absent references.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/AlertsRuleEvaluatorResourceIT.java Adds integration coverage for resolving parent ownership and domains and handling unresolvable parents.
openmetadata-service/src/test/java/org/openmetadata/service/events/subscription/AlertUtilTest.java Updates announcement routing tests to distinguish obsolete thread announcements from first-class announcement entity events.

Sequence Diagram

sequenceDiagram
  participant Event as Thread ChangeEvent
  participant Alert as AlertUtil
  participant Rules as AlertsRuleEvaluator
  participant Parent as Parent Entity
  Event->>Alert: Evaluate subscription
  Alert->>Rules: Apply configured filters
  Rules->>Event: Read Thread.entityRef
  alt FQN, ID, or source filter
    Rules->>Rules: Match reference fields
  else Owner or domain filter
    Rules->>Parent: Resolve referenced entity fields
    Parent-->>Rules: Owners or domains
    Rules->>Rules: Match configured scope
  end
  Rules-->>Alert: Match or non-match
Loading

Reviews (3): Last reviewed commit: "refactor(alerts): drop the per-field sub..." | Re-trigger Greptile

@manerow
manerow requested a review from a team as a code owner July 28, 2026 10:02
@manerow manerow added safe to test Add this label to run secure Github workflows on PRs bug Something isn't working backend alerts and notifications labels Jul 28, 2026
@manerow manerow self-assigned this Jul 28, 2026
@gitar-bot

gitar-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 2 resolved / 2 findings

Replaces permissive thread-event rule bypasses with parent entity reference evaluation in AlertsRuleEvaluator, correctly scoping notifications by FQN, ID, source, owner, and domain. No issues found.

✅ 2 resolved
Edge Case: Malformed filterByEntityId value throws and aborts thread batch

📄 openmetadata-service/src/main/java/org/openmetadata/service/events/subscription/AlertsRuleEvaluator.java:702-714
threadSubjectMatchesId() calls UUID.fromString(id) on each configured id (AlertsRuleEvaluator.java:707); a non-UUID filter value raises IllegalArgumentException that escapes the matcher and aborts the whole change-event batch — the exact #28304 failure mode this PR cites, now newly reachable for THREAD events. This mirrors the pre-existing entity path (line 199) and relies on config validation guaranteeing UUIDs, so impact is low, but the thread path could parse ids defensively (skip/continue on invalid values) to be robust.

Performance: Parent entity re-resolved per matcher for thread events

📄 openmetadata-service/src/main/java/org/openmetadata/service/events/subscription/AlertsRuleEvaluator.java:683-697
For a THREAD event, each scoping matcher independently re-derives the parent: threadSubject() re-deserializes the Thread payload (getThread) on every call, and threadSubjectMatchesOwner/threadSubjectMatchesDomain each issue a separate Entity.getEntityOrNull DB read for the same parent reference. When an alert scopes by both owner and domain this doubles the parent fetch per event. Consider resolving the subject reference once and caching the fetched parent (with the union of needed fields) if thread-event volume is high; low priority since the entity path has the same shape.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar | Powered by Gitar — free for open source

@github-actions

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit 3b40381d5f14a83cca1b0989d80f011047a93981 in Playwright run 30350579937, attempt 1.

✅ 537 passed · ❌ 0 failed · 🟡 0 flaky · ⏭️ 5 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 52m 8s

⏱️ Max setup 3m 1s · max shard execution 17m 44s · max shard-job elapsed before upload 21m 35s · reporting 5s

🌐 202.59 requests/attempt · 2.86 app boots/UI scenario · 4.32% common-shard skew

Optimization targets still in progress:

  • Browser traffic was 202.59 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.86 per UI scenario (1596 boots / 558 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard chromium-01 143 0 0 3 0 0
✅ Shard chromium-02 133 0 0 0 0 0
✅ Shard chromium-03 123 0 0 0 0 0
✅ Shard data-asset-rules-01 61 0 0 0 0 0
✅ Shard domain-isolation-01 14 0 0 0 0 0
✅ Shard global-state-01 23 0 0 0 0 0
✅ Shard ingestion-01 1 0 0 0 0 0
✅ Shard reindex-01 2 0 0 0 0 0
✅ Shard search-01 10 0 0 0 0 0
✅ Shard search-rbac-01 27 0 0 2 0 0

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

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

Labels

alerts and notifications backend bug Something isn't working safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Entity-scoped alert filters are bypassed for thread events (alerts deliver conversations outside their scope)

1 participant