Skip to content

Commit f7c56a6

Browse files
committed
bugsnag: strip JSON objects from grouping strings, normalize LauncherError
Any embedded JSON/dict literal (starting with `{"` or `{'`) is now stripped from error messages before they are used as grouping keys. This prevents Kubernetes pod specs and similar runtime blobs from fragmenting what is structurally the same error into many groups. A dedicated normalizer for LauncherError is also added: it drops everything after the first colon (the serialized pod spec) so that "Failed to create pod: <spec>" normalizes to just "LauncherError: Failed to create pod".
1 parent fa70d6d commit f7c56a6

2 files changed

Lines changed: 64 additions & 0 deletions

File tree

cloud_pipelines_backend/instrumentation/error_normalization.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@
1616
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.IGNORECASE
1717
)
1818
_LONG_ALNUM_ID_PATTERN = re.compile(r"\b[a-zA-Z0-9]{16,}\b")
19+
# Matches any embedded JSON object or Python dict literal (starts with `{"` or `{'`).
20+
# These are stripped from grouping strings because they contain highly variable
21+
# runtime data (e.g. full Kubernetes pod specs) that would fragment error groups.
22+
_JSON_OBJECT_PATTERN = re.compile(r"\{['\"].*", re.DOTALL)
1923

2024

2125
def _strip_generic(*, message: str) -> str:
26+
message = _JSON_OBJECT_PATTERN.sub("{...}", message)
2227
message = _OBJECT_REPR_PATTERN.sub("{object}", message)
2328
message = _HEX_ADDRESS_PATTERN.sub("{addr}", message)
2429
message = _UUID_PATTERN.sub("{uuid}", message)
@@ -85,13 +90,27 @@ def _normalize_orchestrator_error(*, exception: BaseException) -> str | None:
8590
return f"OrchestratorError: {message}"
8691

8792

93+
def _normalize_launcher_error(*, exception: BaseException) -> str | None:
94+
try:
95+
from ..launchers.interfaces import LauncherError
96+
except ImportError:
97+
return None
98+
if not isinstance(exception, LauncherError):
99+
return None
100+
# Take only the verb phrase before the first colon to drop any embedded
101+
# serialized data (e.g. the full Kubernetes pod spec appended after ": ").
102+
head = str(exception).split(":", 1)[0].strip()
103+
return f"LauncherError: {head}"
104+
105+
88106
def normalize_error_message(*, exception: BaseException) -> str:
89107
"""Return a stable normalized string for error grouping."""
90108
for normalizer in (
91109
_normalize_k8s_api_exception,
92110
_normalize_max_retry_error,
93111
_normalize_unicode_decode_error,
94112
_normalize_orchestrator_error,
113+
_normalize_launcher_error,
95114
):
96115
result = normalizer(exception=exception)
97116
if result is not None:

tests/instrumentation/test_error_normalization.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,41 @@ def test_strips_object_repr(self):
184184
)
185185

186186

187+
class TestNormalizeLauncherError:
188+
def _make_launcher_error(
189+
self, message: str, cause: BaseException | None = None
190+
) -> Exception:
191+
try:
192+
from cloud_pipelines_backend.launchers.interfaces import LauncherError
193+
except ImportError:
194+
pytest.skip("LauncherError not importable")
195+
if cause:
196+
try:
197+
raise LauncherError(message) from cause
198+
except LauncherError as exc:
199+
return exc
200+
return LauncherError(message)
201+
202+
def test_strips_pod_spec_json(self):
203+
pod_spec = (
204+
"{'apiVersion': 'v1', 'kind': 'Pod', 'metadata': {'name': 'task-abc-xyz'}}"
205+
)
206+
exc = self._make_launcher_error(f"Failed to create pod: {pod_spec}")
207+
result = error_normalization.normalize_error_message(exception=exc)
208+
assert result == "LauncherError: Failed to create pod"
209+
210+
def test_with_timeout_cause(self):
211+
cause = TimeoutError("The read operation timed out")
212+
exc = self._make_launcher_error("Failed to create pod: {big spec}", cause=cause)
213+
result = error_normalization.normalize_error_message(exception=exc)
214+
assert result == "LauncherError: Failed to create pod"
215+
216+
def test_no_colon_in_message(self):
217+
exc = self._make_launcher_error("launch failed")
218+
result = error_normalization.normalize_error_message(exception=exc)
219+
assert result == "LauncherError: launch failed"
220+
221+
187222
class TestFallback:
188223
def test_strips_hex_address(self):
189224
exc = ValueError("object at 0xdeadbeef failed")
@@ -204,3 +239,13 @@ def test_stable_message_unchanged(self):
204239
exc = AttributeError("'NoneType' object has no attribute 'encode'")
205240
result = error_normalization.normalize_error_message(exception=exc)
206241
assert result == "AttributeError: 'NoneType' object has no attribute 'encode'"
242+
243+
def test_strips_json_object(self):
244+
exc = RuntimeError("operation failed: {'key': 'value', 'nested': {'a': 1}}")
245+
result = error_normalization.normalize_error_message(exception=exc)
246+
assert result == "RuntimeError: operation failed: {...}"
247+
248+
def test_strips_json_object_double_quotes(self):
249+
exc = RuntimeError('operation failed: {"key": "value"}')
250+
result = error_normalization.normalize_error_message(exception=exc)
251+
assert result == "RuntimeError: operation failed: {...}"

0 commit comments

Comments
 (0)