fix(alerts): scope thread events by their parent entity instead of bypassing filters - #30571
Conversation
✅ Playwright Results — workflow succeededValidated commit ✅ 770 passed · ❌ 0 failed · 🟡 1 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky PerformanceBlocking 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:
🟡 1 flaky test(s) (passed on retry)
How to debug locally# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip # view trace |
Code Review ✅ Approved 2 resolved / 2 findingsScopes 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
✅ Performance: Parent entity re-resolved per matcher for thread events
OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
…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
…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
Fixes #30555
Problem
An alert scoped to one entity receives conversations about every entity of that type. Reproduced on
a clean
maindeploy: an alert withresource: glossaryTerm,filterByEventType: [threadCreated, postCreated]and
filterByFqn: [<term T2>]received both events for a conversation started on T1.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 = THREADand aThreadpayload rather than the entity thethread is about, so five matchers in
AlertsRuleEvaluatorshort-circuited:Returning
truefrom a filter that cannot be evaluated means "deliver", not "does not match". Thatwas unreachable for entity resources until #28122 correctly routed thread events to the alert of the
entity the thread is about, at which point
glossaryTerm,tableand friends, which do offer thesefilters, started hitting the bypass.
Fix
The event already carries the answer:
Thread.entityRefpoints at the parent entity. Each bypass nowevaluates the same question against that reference.
THREADeventmatchAnyEntityFqnmatchesFqnOrDescendant(thread.entityRef.fullyQualifiedName, ...), so ancestor scoping from #28833 keeps workingmatchAnyEntityIdthread.entityRef.id, skipping filter values that are not valid UUIDsmatchAnySourcethread.entityRef.typematchAnyOwnerNameownersand reuses the existingmatchOwners(...)matchAnyDomaindomainsthrough the sharedmatchesAnyDomainFqn(...), also used by the entity pathOwner 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
falseinstead 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.
getEntityOrNullnarrows its catch toEntityNotFoundException, so asking for a field the parent's schema does not declare still throws; thatis 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
falsesince #29112 and are untouched.The parent reference is resolved once per event and memoized, so a condition chaining several scoping
filters parses the
Threadpayload only once. Entity id filter values that are not valid UUIDs areskipped rather than allowed to throw out of
UUID.fromString, for the same batch-abort reason.matchAnySourceis worth calling out becausefilterBySourceappears in no resource'ssupportedFilters: its only consumer is the seededActivityFeedAlert. That subscription is unaffected.Its condition reduces to
R1 || (R2 && R3 && !isBot()), and for a thread eventR1is false(
matchAnyEventType({entityCreated, entityDeleted, entitySoftDeleted})) andR2is false (nochangeDescriptionnaming a watched field), so the expression was already false before this changeregardless of what
R3, thematchAnySourcerule, returns.Also drops the dead
announcemententry fromAlertUtil.THREAD_TYPE_RESOURCES. Announcement has been afirst-class entity since #25894, its events carry
entityType = announcementand take the generic path,and
FeedResource.rejectLegacyAnnouncementAccessblocks the legacy door.taskstays until the lastlegacy 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-resourcesupportedFilters), soevery stored rule keeps parsing and evaluating. Only the meaning for
THREADevents 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 noentityRefwhere every scoping filter must returnfalse.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 anannouncementresource still matches realentityType=announcementevents.Verified end to end on a local deploy
filterByFqn: [T2]filterByOwnerName: [admin], T1 is admin-ownedfilterByFqn: [nonexistent]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
actionsis non-empty. Such an alert does receive thread events, and now honours itsfilters 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: 1for the in-scopecomment only.
Greptile Summary
This PR scopes thread-event alert filters to the entity referenced by the thread.
Thread.entityRef.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
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]Reviews (4): Last reviewed commit: "Merge branch 'main' into fix/alert-threa..." | Re-trigger Greptile
Context used: