Skip to content

Commit 394c09c

Browse files
authored
bugsnag: strip JSON objects from grouping strings, normalize LauncherError (#261)
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 479f3e9 commit 394c09c

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

cloud_pipelines_backend/instrumentation/error_normalization.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,30 @@
99
import json
1010
import re
1111

12+
try:
13+
from ..launchers.interfaces import LauncherError as _LauncherError
14+
15+
_LAUNCHER_ERROR_AVAILABLE = True
16+
except ImportError:
17+
_LauncherError = None # type: ignore[assignment,misc]
18+
_LAUNCHER_ERROR_AVAILABLE = False
19+
1220
_POD_NAME_PATTERN = re.compile(r"(?:task|tangle(?:-ce)?)-[a-zA-Z0-9]+-[a-zA-Z0-9]+")
1321
_OBJECT_REPR_PATTERN = re.compile(r"<[^>]+ object at 0x[0-9a-fA-F]+>")
1422
_HEX_ADDRESS_PATTERN = re.compile(r"\b0x[0-9a-fA-F]+\b")
1523
_UUID_PATTERN = re.compile(
1624
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.IGNORECASE
1725
)
1826
_LONG_ALNUM_ID_PATTERN = re.compile(r"\b[a-zA-Z0-9]{16,}\b")
27+
# Matches from the first `{"`, `{'`, or `{ "` / `{ '` to end of string.
28+
# Both the embedded dict/JSON literal and any trailing message text are replaced
29+
# with `{...}` — the greedy match is intentional: anything after a runtime-data
30+
# dict in an error message is typically also variable and should not affect grouping.
31+
_JSON_OBJECT_PATTERN = re.compile(r"\{\s*['\"].*", re.DOTALL)
1932

2033

2134
def _strip_generic(*, message: str) -> str:
35+
message = _JSON_OBJECT_PATTERN.sub("{...}", message)
2236
message = _OBJECT_REPR_PATTERN.sub("{object}", message)
2337
message = _HEX_ADDRESS_PATTERN.sub("{addr}", message)
2438
message = _UUID_PATTERN.sub("{uuid}", message)
@@ -85,13 +99,21 @@ def _normalize_orchestrator_error(*, exception: BaseException) -> str | None:
8599
return f"OrchestratorError: {message}"
86100

87101

102+
def _normalize_launcher_error(*, exception: BaseException) -> str | None:
103+
if not _LAUNCHER_ERROR_AVAILABLE or not isinstance(exception, _LauncherError):
104+
return None
105+
message = _JSON_OBJECT_PATTERN.sub("{...}", str(exception))
106+
return f"LauncherError: {message.strip()}"
107+
108+
88109
def normalize_error_message(*, exception: BaseException) -> str:
89110
"""Return a stable normalized string for error grouping."""
90111
for normalizer in (
91112
_normalize_k8s_api_exception,
92113
_normalize_max_retry_error,
93114
_normalize_unicode_decode_error,
94115
_normalize_orchestrator_error,
116+
_normalize_launcher_error,
95117
):
96118
result = normalizer(exception=exception)
97119
if result is not None:

tests/instrumentation/test_error_normalization.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,52 @@ 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(
213+
"Failed to create pod: {'apiVersion': 'v1'}", cause=cause
214+
)
215+
result = error_normalization.normalize_error_message(exception=exc)
216+
assert result == "LauncherError: Failed to create pod: {...}"
217+
218+
def test_no_colon_in_message(self):
219+
exc = self._make_launcher_error("launch failed")
220+
result = error_normalization.normalize_error_message(exception=exc)
221+
assert result == "LauncherError: launch failed"
222+
223+
def test_multi_colon_diagnostic_preserved(self):
224+
exc = self._make_launcher_error(
225+
"creating pod: spec invalid: missing field 'name'"
226+
)
227+
result = error_normalization.normalize_error_message(exception=exc)
228+
assert (
229+
result == "LauncherError: creating pod: spec invalid: missing field 'name'"
230+
)
231+
232+
187233
class TestFallback:
188234
def test_strips_hex_address(self):
189235
exc = ValueError("object at 0xdeadbeef failed")
@@ -204,3 +250,13 @@ def test_stable_message_unchanged(self):
204250
exc = AttributeError("'NoneType' object has no attribute 'encode'")
205251
result = error_normalization.normalize_error_message(exception=exc)
206252
assert result == "AttributeError: 'NoneType' object has no attribute 'encode'"
253+
254+
def test_strips_json_object(self):
255+
exc = RuntimeError("operation failed: {'key': 'value', 'nested': {'a': 1}}")
256+
result = error_normalization.normalize_error_message(exception=exc)
257+
assert result == "RuntimeError: operation failed: {...}"
258+
259+
def test_strips_json_object_double_quotes(self):
260+
exc = RuntimeError('operation failed: {"key": "value"}')
261+
result = error_normalization.normalize_error_message(exception=exc)
262+
assert result == "RuntimeError: operation failed: {...}"

0 commit comments

Comments
 (0)