Skip to content

Commit c5e88c5

Browse files
authored
fix: dismiss until (#5281)
1 parent c948540 commit c5e88c5

File tree

5 files changed

+96
-27
lines changed

5 files changed

+96
-27
lines changed

keep-ui/next_build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
echo "Env vars:"
55
env
66
echo "Building"
7-
next build
7+
NODE_OPTIONS="--max-old-space-size=8192" next build

keep/api/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
SCHEDULER = config("SCHEDULER", default="true", cast=bool)
8282
CONSUMER = config("CONSUMER", default="true", cast=bool)
8383
TOPOLOGY = config("KEEP_TOPOLOGY_PROCESSOR", default="false", cast=bool)
84+
WATCHER = config("WATCHER", default="false", cast=bool)
8485
KEEP_DEBUG_TASKS = config("KEEP_DEBUG_TASKS", default="false", cast=bool)
8586
KEEP_DEBUG_MIDDLEWARES = config("KEEP_DEBUG_MIDDLEWARES", default="false", cast=bool)
8687
KEEP_USE_LIMITER = config("KEEP_USE_LIMITER", default="false", cast=bool)
@@ -160,7 +161,7 @@ async def startup():
160161
except Exception:
161162
logger.exception("Failed to start the topology processor")
162163

163-
if MAINTENANCE_WINDOWS and MAINTENANCE_WINDOW_ALERT_STRATEGY == "recover_previous_status":
164+
if WATCHER or (MAINTENANCE_WINDOWS and MAINTENANCE_WINDOW_ALERT_STRATEGY == "recover_previous_status"):
164165
if REDIS:
165166
try:
166167
logger.info("Starting the watcher process")

keep/api/bl/dismissal_expiry_bl.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,17 @@ def get_alerts_with_expired_dismissals(session: Session) -> List[AlertEnrichment
4949

5050
# Build cross-database compatible boolean comparison
5151
# Different databases store/extract JSON booleans differently:
52-
# - SQLite: json_extract returns 1/0 for true/false
53-
# - MySQL: JSON_UNQUOTE(JSON_EXTRACT()) returns "true"/"false" strings
54-
# - PostgreSQL: ->> operator returns "true"/"false" strings
52+
# - SQLite: json_extract can return 1/0 for true/false OR "True"/"False"/"true"/"false" strings depending on how data was stored
53+
# - MySQL: JSON_UNQUOTE(JSON_EXTRACT()) returns "true"/"false" strings (lowercase)
54+
# - PostgreSQL: json_extract_path_text() returns "true"/"false" strings (lowercase) OR "True"/"False" (depending on input)
5555
if session.bind.dialect.name == "sqlite":
56-
dismissed_condition = dismissed_field == 1
56+
# Handle both integer and string representations in SQLite
57+
dismissed_condition = (dismissed_field == 1) | (dismissed_field == "True") | (dismissed_field == "true")
58+
elif session.bind.dialect.name == "postgresql":
59+
# PostgreSQL can return both "true"/"false" and "True"/"False" depending on how the data was stored
60+
dismissed_condition = (dismissed_field == "true") | (dismissed_field == "True")
5761
else:
58-
# For MySQL and PostgreSQL, compare with string "true"
62+
# For MySQL, compare with lowercase string "true"
5963
dismissed_condition = dismissed_field == "true"
6064

6165
query = session.exec(

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "keep"
3-
version = "0.47.4"
3+
version = "0.47.5"
44
description = "Alerting. for developers, by developers."
55
authors = ["Keep Alerting LTD"]
66
packages = [{include = "keep"}]

tests/test_dismissal_expiry_bug.py

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,12 @@ def test_dismissal_validation_at_creation_time():
104104

105105
def test_dismissal_expiry_bug_search_filters(db_session):
106106
"""
107-
Test that reproduces the dismissedUntil expiry bug with search filters.
107+
Test that verifies the dismissedUntil expiry fix works correctly with search filters.
108108
109-
This test demonstrates the bug where alerts with expired dismissedUntil
110-
don't appear in searches for dismissed == false.
109+
This test demonstrates that alerts with expired dismissedUntil properly appear
110+
in searches for dismissed == false after the watcher processes them.
111111
112-
This test should FAIL due to the bug (without watcher running).
112+
This test should PASS with the fixed watcher implementation.
113113
"""
114114
tenant_id = SINGLE_TENANT_UUID
115115

@@ -200,14 +200,14 @@ def test_dismissal_expiry_bug_search_filters(db_session):
200200
cel_query="dismissed == false", # or !dismissed
201201
)
202202

203-
# BUG: This should return the alert because its dismissal has expired,
204-
# but it won't because there's no background process updating the status
203+
# Before watcher: Alert still appears dismissed in database because
204+
# the enrichment hasn't been updated yet (dismissal expired but database not updated)
205205
non_dismissed_alerts_before = SearchEngine(tenant_id=tenant_id).search_alerts(search_query_not_dismissed)
206206

207-
# Demonstrate the bug exists - should return 0 because bug prevents proper filtering
207+
# Before watcher runs: Database still shows alert as dismissed (expected behavior)
208208
assert len(non_dismissed_alerts_before) == 0, (
209-
"Bug reproduction confirmed: Alert doesn't appear in non-dismissed filter after "
210-
"dismissUntil expires (this is the bug we're fixing)"
209+
"Before watcher: Alert doesn't appear in non-dismissed filter because "
210+
"database hasn't been updated yet (dismissal expired but enrichment not processed)"
211211
)
212212

213213
# NOW APPLY THE FIX: Run dismissal expiry watcher
@@ -216,7 +216,7 @@ def test_dismissal_expiry_bug_search_filters(db_session):
216216
# AFTER FIX: The alert should now appear correctly
217217
non_dismissed_alerts_after = SearchEngine(tenant_id=tenant_id).search_alerts(search_query_not_dismissed)
218218

219-
# This should now PASS - proving our fix works
219+
# After watcher runs: Alert should now appear in non-dismissed filter
220220
assert len(non_dismissed_alerts_after) == 1, (
221221
"FIXED: Alert now appears in non-dismissed filter after watcher processes expired dismissal"
222222
)
@@ -226,10 +226,10 @@ def test_dismissal_expiry_bug_search_filters(db_session):
226226

227227
def test_dismissal_expiry_bug_sidebar_filter(db_session):
228228
"""
229-
Test that reproduces the bug specifically with sidebar "Not dismissed" filter.
229+
Test that verifies sidebar "Not dismissed" filter works correctly after watcher processes expired dismissals.
230230
231231
This simulates the UI scenario described in the GitHub issue.
232-
This test should FAIL due to the bug.
232+
This test should PASS with the fixed watcher implementation.
233233
"""
234234
tenant_id = SINGLE_TENANT_UUID
235235

@@ -395,9 +395,9 @@ def test_dismissal_expiry_bug_sidebar_filter(db_session):
395395

396396
def test_dismissal_expiry_bug_with_cel_filter(db_session):
397397
"""
398-
Test the bug specifically with CEL-based filters like dismissed == false.
398+
Test that CEL-based filters work correctly with dismissed == false after watcher processes expired dismissals.
399399
400-
This test should FAIL due to the bug.
400+
This test should PASS with the fixed watcher implementation.
401401
"""
402402
tenant_id = SINGLE_TENANT_UUID
403403

@@ -475,13 +475,13 @@ def test_dismissal_expiry_bug_with_cel_filter(db_session):
475475
cel_query=cel_expr,
476476
)
477477

478-
# BUG: All of these should return 1 alert, but will return 0
478+
# Before watcher: Database hasn't been updated yet, so filter won't find the alert
479479
results_before = SearchEngine(tenant_id=tenant_id).search_alerts(search_query)
480480

481-
# Demonstrate the bug exists - should return 0 due to bug
481+
# Before watcher runs: Database still shows alert as dismissed
482482
assert len(results_before) == 0, (
483-
f"Bug reproduction: CEL expression '{cel_expr}' should return 1 alert "
484-
f"after dismissal expires, but returns {len(results_before)} due to bug"
483+
f"Before watcher: CEL expression '{cel_expr}' returns 0 because "
484+
f"database hasn't been updated yet (dismissal expired but not processed)"
485485
)
486486

487487
# APPLY THE FIX: Run dismissal expiry watcher
@@ -500,7 +500,7 @@ def test_dismissal_expiry_bug_with_cel_filter(db_session):
500500
# Should now return 1 alert correctly
501501
results_after = SearchEngine(tenant_id=tenant_id).search_alerts(search_query)
502502

503-
# This should now PASS - proving our fix works
503+
# After watcher runs: Should now work correctly
504504
assert len(results_after) == 1, (
505505
f"FIXED: CEL expression '{cel_expr}' now correctly returns 1 alert "
506506
f"after watcher processes expired dismissal"
@@ -776,6 +776,70 @@ def test_dismissal_expiry_bug_fixed_with_watcher(db_session):
776776
getattr(non_dismissed_alerts_after[0], 'disposable_dismissed', None) is None
777777

778778

779+
def test_dismissal_expiry_boolean_comparison_fix(db_session):
780+
"""
781+
Test that specifically validates the boolean comparison fix in get_alerts_with_expired_dismissals.
782+
783+
This test ensures that the function correctly finds dismissed alerts stored with different boolean
784+
formats across different database types, which was the root cause of the original bug.
785+
"""
786+
from keep.api.bl.dismissal_expiry_bl import DismissalExpiryBl
787+
import datetime
788+
from keep.api.models.db.alert import AlertEnrichment
789+
790+
tenant_id = SINGLE_TENANT_UUID
791+
current_time = datetime.datetime.now(datetime.timezone.utc)
792+
past_time = current_time - datetime.timedelta(hours=1)
793+
past_time_str = past_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
794+
795+
# Test different boolean representations that might be encountered
796+
test_cases = [
797+
{
798+
"fingerprint": "boolean-test-True",
799+
"dismissed": "True", # Capitalized string (common in JavaScript/Python serialization)
800+
"description": "Capitalized 'True' string"
801+
},
802+
{
803+
"fingerprint": "boolean-test-true",
804+
"dismissed": "true", # Lowercase string (JSON standard)
805+
"description": "Lowercase 'true' string"
806+
}
807+
]
808+
809+
created_enrichments = []
810+
for test_case in test_cases:
811+
enrichment = AlertEnrichment(
812+
tenant_id=tenant_id,
813+
alert_fingerprint=test_case["fingerprint"],
814+
enrichments={
815+
"dismissed": test_case["dismissed"],
816+
"dismissUntil": past_time_str, # Expired timestamp
817+
"status": "suppressed"
818+
},
819+
timestamp=current_time
820+
)
821+
created_enrichments.append(enrichment)
822+
db_session.add(enrichment)
823+
824+
db_session.commit()
825+
826+
# Test that get_alerts_with_expired_dismissals finds all variations
827+
expired_dismissals = DismissalExpiryBl.get_alerts_with_expired_dismissals(db_session)
828+
found_fingerprints = {e.alert_fingerprint for e in expired_dismissals}
829+
830+
# Verify all test cases are found regardless of boolean format
831+
for test_case in test_cases:
832+
assert test_case["fingerprint"] in found_fingerprints, (
833+
f"Boolean comparison fix should find dismissed alerts with {test_case['description']} "
834+
f"(found: {found_fingerprints})"
835+
)
836+
837+
# Verify the found enrichment has the expected data
838+
test_found = next(e for e in expired_dismissals if e.alert_fingerprint == test_case["fingerprint"])
839+
assert test_found.enrichments["dismissed"] == test_case["dismissed"]
840+
assert test_found.enrichments["dismissUntil"] == past_time_str
841+
842+
779843
def test_github_issue_5047_cel_filters_dismisseduntil_bug_fixed(db_session):
780844
"""
781845
Explicit test that solves GitHub Issue #5047:

0 commit comments

Comments
 (0)