Skip to content

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

Merged
manerow merged 4 commits into
mainfrom
fix/alert-thread-scoping-filters
Aug 11, 2026
Merged

fix(alerts): scope thread events by their parent entity instead of bypassing filters#30571
manerow merged 4 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, skipping filter values that are not valid UUIDs
matchAnySource compares thread.entityRef.type
matchAnyOwnerName resolves the parent with owners and reuses the existing matchOwners(...)
matchAnyDomain resolves the parent with domains through the shared matchesAnyDomainFqn(...), 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 unregistered 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. Those are the three cases verified. getEntityOrNull narrows its catch to
EntityNotFoundException, so asking for a field the parent's schema does not declare still throws; that
is pre-existing on the entity path (readStoredEntity) and is tracked separately in #31331.

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

The parent reference is resolved once per event and memoized, so a condition chaining several scoping
filters parses the Thread payload only once. Entity id filter values that are not valid UUIDs are
skipped rather than allowed to throw out of UUID.fromString, for the same batch-abort reason.

matchAnySource is worth calling out because filterBySource appears in no resource's
supportedFilters: its only consumer is the seeded ActivityFeedAlert. That subscription is unaffected.
Its condition reduces to R1 || (R2 && R3 && !isBot()), and for a thread event R1 is false
(matchAnyEventType({entityCreated, entityDeleted, entitySoftDeleted})) and R2 is false (no
changeDescription naming a watched field), so the expression was already false before this change
regardless of what R3, the matchAnySource rule, returns.

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, 10 cases): FQN exact / ancestor / sibling-prefix / non-match, entity id, source, a malformed entity id filter value, 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

This PR scopes thread-event alert filters to the entity referenced by the thread.

  • Evaluates source, owner, FQN, entity ID, and domain filters against Thread.entityRef.
  • Resolves parent entities when owner or domain data is required and treats absent or unresolved parents as non-matches.
  • Removes announcements from legacy thread-resource routing while retaining task and conversation handling.
  • Adds unit and integration coverage for thread scoping, malformed IDs, missing parents, announcements, owners, and domains.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains within the scope of the available follow-up threads.

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/events/subscription/AlertsRuleEvaluator.java Replaces unconditional thread-event filter matches with parent-reference scoping and shared ID/domain matching helpers.
openmetadata-service/src/main/java/org/openmetadata/service/events/subscription/AlertUtil.java Removes announcement from the set of resources routed through the legacy thread-type path.
openmetadata-service/src/test/java/org/openmetadata/service/events/subscription/AlertsRuleEvaluatorThreadScopeTest.java Adds focused coverage for parent FQN, ID, type, malformed IDs, and absent entity references.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/AlertsRuleEvaluatorResourceIT.java Adds real-entity coverage for thread parent owner, domain, FQN, and unresolved-parent behavior.
openmetadata-service/src/test/java/org/openmetadata/service/events/subscription/AlertUtilTest.java Updates announcement routing tests to distinguish first-class announcement events from legacy announcement threads.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  E[Thread ChangeEvent] --> T[Read Thread.entityRef]
  T --> F[FQN / ID / source filters]
  T --> P[Resolve parent entity]
  P --> O[Owner filter]
  P --> D[Domain filter]
  F --> R{Filter matches?}
  O --> R
  D --> R
  R -->|Yes| A[Deliver alert]
  R -->|No| X[Suppress alert]
Loading

Reviews (4): Last reviewed commit: "Merge branch 'main' into fix/alert-threa..." | Re-trigger Greptile

Context used:

@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
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit 520fd92f417c528a99a863cab6fe3ca6367f432e in Playwright run 31482396925, attempt 1.

✅ 770 passed · ❌ 0 failed · 🟡 1 flaky · ⏭️ 0 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 14s

⏱️ Max setup 3m 7s · max shard execution 18m 0s · max shard-job elapsed before upload 21m 53s · reporting 5s

🌐 207.08 requests/attempt · 2.65 app boots/UI scenario · 8.89% common-shard skew

Optimization targets still in progress:

  • Browser traffic was 207.08 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.65 per UI scenario (2133 boots / 804 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard chromium-01 124 0 0 0 0 0
✅ Shard chromium-02 145 0 0 0 0 0
✅ Shard chromium-03 149 0 0 0 0 0
🟡 Shard chromium-04 141 0 1 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 34 0 0 0 0 0
✅ Shard import-export-01 26 0 0 0 0 0
✅ Shard ingestion-01 32 0 0 0 0 0
✅ Shard reindex-01 5 0 0 0 0 0
✅ Shard search-01 10 0 0 0 0 0
✅ Shard search-rbac-01 29 0 0 0 0 0
🟡 1 flaky test(s) (passed on retry)
  • Pages/Entity.spec.tsAnnouncement create, edit & delete (shard chromium-04, 1 retry)

📦 Download artifacts

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

@manerow
manerow added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit 75d6530 Aug 11, 2026
110 checks passed
@manerow
manerow deleted the fix/alert-thread-scoping-filters branch August 11, 2026 15:54
@gitar-bot

gitar-bot Bot commented Aug 11, 2026

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

Scopes alert thread events by their parent entity instead of bypassing filters, addressing malformed entity IDs and redundant parent resolutions. 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 👍 / 👎 | Powered by Gitar — free for open source

manerow added a commit that referenced this pull request Aug 12, 2026
…passing filters (#30571)

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

* fix(alerts): memoize the thread subject and skip non-UUID entity id filter values

* refactor(alerts): drop the per-field subject cache and its wrapper

* refactor(alerts): drop the unreachable null guard in threadSubject
k-anshul pushed a commit to k-anshul/OpenMetadata that referenced this pull request Aug 12, 2026
…passing filters (open-metadata#30571)

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

* fix(alerts): memoize the thread subject and skip non-UUID entity id filter values

* refactor(alerts): drop the per-field subject cache and its wrapper

* refactor(alerts): drop the unreachable null guard in threadSubject
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)

2 participants