From d81b2d67ec2989fecd555aa6c8a96963d8d06c96 Mon Sep 17 00:00:00 2001 From: zerafachris Date: Fri, 17 Jul 2026 17:27:57 +0200 Subject: [PATCH] fix: truncate large test result samples to prevent oversized alert payloads (#2044) Test result samples rendered as an alert table had no upper bound on row count. Alerts with many failed/warning rows (e.g. 25+ rows with several columns) could grow large enough to be rejected by downstream integrations such as Microsoft Teams, causing edr monitor to crash before sending any alert. Cap the rendered sample at MessageBuilderConfig.maximum_rows_in_alert_samples (default 25, mirroring the existing maximum_columns_in_alert_samples knob) and append a note indicating how many rows were omitted when truncation occurs. Co-Authored-By: Claude Sonnet 5 --- .../monitor/alerts/alert_messages/builder.py | 25 ++++- .../test_result_blocks_row_truncation.py | 92 +++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 tests/unit/alerts/alert_messages/test_result_blocks_row_truncation.py diff --git a/elementary/monitor/alerts/alert_messages/builder.py b/elementary/monitor/alerts/alert_messages/builder.py index 486cf9692..bc7d06760 100644 --- a/elementary/monitor/alerts/alert_messages/builder.py +++ b/elementary/monitor/alerts/alert_messages/builder.py @@ -57,6 +57,7 @@ class MessageBuilderConfig(BaseModel): alert_groups_subscribers: bool = False maximum_columns_in_alert_samples: int = 4 + maximum_rows_in_alert_samples: int = 25 class AlertMessageBuilder: @@ -377,9 +378,31 @@ def _get_result_blocks( and len(result_sample[0].keys()) <= self.config.maximum_columns_in_alert_samples ): + rows_to_render = result_sample + omitted_rows_count = 0 + if len(result_sample) > self.config.maximum_rows_in_alert_samples: + rows_to_render = result_sample[ + : self.config.maximum_rows_in_alert_samples + ] + omitted_rows_count = len(result_sample) - len(rows_to_render) result_blocks.append( - TableBlock.from_dicts(result_sample), + TableBlock.from_dicts(rows_to_render), ) + if omitted_rows_count: + result_blocks.append( + LinesBlock( + lines=[ + ItalicTextLineBlock( + text=( + f"Showing {len(rows_to_render)} of " + f"{len(result_sample)} rows " + f"({omitted_rows_count} omitted to keep " + "the alert size manageable)." + ) + ), + ] + ) + ) else: result_blocks.append( JsonCodeBlock(content=result_sample), diff --git a/tests/unit/alerts/alert_messages/test_result_blocks_row_truncation.py b/tests/unit/alerts/alert_messages/test_result_blocks_row_truncation.py new file mode 100644 index 000000000..5e233d01d --- /dev/null +++ b/tests/unit/alerts/alert_messages/test_result_blocks_row_truncation.py @@ -0,0 +1,92 @@ +from typing import List + +from elementary.messages.blocks import ExpandableBlock, LinesBlock, TableBlock +from elementary.monitor.alerts.alert_messages.builder import MessageBuilderConfig +from tests.unit.alerts.alert_messages.test_alert_utils import ( + build_base_test_alert_model, + get_alert_message_body, +) + + +def _build_test_rows_sample(row_count: int) -> List[dict]: + return [{"column1": f"value_{i}", "column2": i} for i in range(row_count)] + + +def _get_test_result_expandable(message_body) -> ExpandableBlock: + for block in message_body.blocks: + if isinstance(block, ExpandableBlock) and block.title == "Test Result": + return block + raise AssertionError("Expected a 'Test Result' expandable block") + + +def _get_table_block(blocks) -> TableBlock: + for block in blocks: + if isinstance(block, TableBlock): + return block + raise AssertionError("Expected a table block among the result blocks") + + +def _extract_line_texts(blocks) -> List[str]: + texts: List[str] = [] + for block in blocks: + if not isinstance(block, LinesBlock): + continue + for line in block.lines: + for inline in line.inlines: + text = getattr(inline, "text", None) + if text: + texts.append(text) + return texts + + +def test_large_result_sample_is_truncated_to_configured_row_limit(): + config = MessageBuilderConfig(maximum_rows_in_alert_samples=10) + alert = build_base_test_alert_model( + status="fail", + table_name="test_table", + tags=None, + owners=None, + subscribers=None, + test_description=None, + error_message=None, + test_rows_sample=_build_test_rows_sample(37), + test_results_query=None, + test_params=None, + ) + + message_body = get_alert_message_body(alert, config=config) + result_body = _get_test_result_expandable(message_body).body + + table_block = _get_table_block(result_body) + assert len(table_block.rows) == 10 + + truncation_notes = _extract_line_texts(result_body) + assert any( + "Showing 10 of 37 rows" in text and "27 omitted" in text + for text in truncation_notes + ), f"Expected a truncation note, got: {truncation_notes}" + + +def test_small_result_sample_is_not_truncated(): + config = MessageBuilderConfig(maximum_rows_in_alert_samples=10) + alert = build_base_test_alert_model( + status="fail", + table_name="test_table", + tags=None, + owners=None, + subscribers=None, + test_description=None, + error_message=None, + test_rows_sample=_build_test_rows_sample(3), + test_results_query=None, + test_params=None, + ) + + message_body = get_alert_message_body(alert, config=config) + result_body = _get_test_result_expandable(message_body).body + + table_block = _get_table_block(result_body) + assert len(table_block.rows) == 3 + + truncation_notes = _extract_line_texts(result_body) + assert not any("omitted" in text for text in truncation_notes)