From abe0fb02f90d9bc187d3426734252603f9b1becc Mon Sep 17 00:00:00 2001 From: Morgan Wowk Date: Thu, 28 May 2026 15:38:32 -0700 Subject: [PATCH] bugsnag: emit slice_name from service_name in custom metadata tab Observe error analytics filters on custom.slice_name to assign errors to a slice. Store the service_name passed to setup() and write it as custom.slice_name on every event so errors from orchestrator, api, and metrics_poller each land in their own slice. Consolidates all custom tab writes into a single add_tab call. --- .../bugsnag_instrumentation.py | 14 +++++++++-- tests/instrumentation/test_bugsnag.py | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/cloud_pipelines_backend/instrumentation/bugsnag_instrumentation.py b/cloud_pipelines_backend/instrumentation/bugsnag_instrumentation.py index d0d47e0..793e1d1 100644 --- a/cloud_pipelines_backend/instrumentation/bugsnag_instrumentation.py +++ b/cloud_pipelines_backend/instrumentation/bugsnag_instrumentation.py @@ -35,6 +35,7 @@ IS_BUGSNAG_ENABLED: bool = bool(_BUGSNAG_API_KEY) _setup_called: bool = False +_service_name: str | None = None def _before_notify(event: bugsnag_event.Event) -> None: @@ -42,6 +43,11 @@ def _before_notify(event: bugsnag_event.Event) -> None: context = contextual_logging.get_all_context_metadata() if context: event.add_tab("tangle_context", context) + + custom: dict[str, str] = {} + if _service_name: + custom["slice_name"] = _service_name + if _CUSTOM_GROUPING_KEY and event.original_error: # Use the full chain for grouping so that "LauncherError <- TimeoutError" # and "LauncherError <- ApiException" land in separate, stable groups. @@ -50,7 +56,7 @@ def _before_notify(event: bugsnag_event.Event) -> None: ) prefix = (event.metadata.get("extra") or {}).get("grouping_prefix") key_value = f"{prefix}: {chain}" if prefix else chain - event.add_tab("custom", {_CUSTOM_GROUPING_KEY: key_value}) + custom[_CUSTOM_GROUPING_KEY] = key_value if prefix and event.errors: try: for error in event.errors: @@ -72,6 +78,9 @@ def _before_notify(event: bugsnag_event.Event) -> None: except Exception: _logger.debug("Could not set chain title on errorClass", exc_info=True) + if custom: + event.add_tab("custom", custom) + def setup(*, service_name: str | None = None) -> None: """Configure the Bugsnag client. @@ -102,8 +111,9 @@ def setup(*, service_name: str | None = None) -> None: project_root=service_name, ) bugsnag_sdk.before_notify(_before_notify) - global _setup_called + global _setup_called, _service_name _setup_called = True + _service_name = service_name except Exception: _logger.exception("Failed to initialize Bugsnag") diff --git a/tests/instrumentation/test_bugsnag.py b/tests/instrumentation/test_bugsnag.py index d40fdd9..74413ab 100644 --- a/tests/instrumentation/test_bugsnag.py +++ b/tests/instrumentation/test_bugsnag.py @@ -271,6 +271,31 @@ def test_before_notify_skips_error_class_prefix_gracefully_on_bad_errors_structu bugsnag_module._before_notify(mock_event) +def test_before_notify_sets_slice_name_when_service_name_configured(monkeypatch): + monkeypatch.setenv("TANGLE_BUGSNAG_API_KEY", "test-api-key") + monkeypatch.setenv("TANGLE_ENV", "staging") + + import importlib + import cloud_pipelines_backend.instrumentation.bugsnag_instrumentation as bugsnag_module + + importlib.reload(bugsnag_module) + + from cloud_pipelines_backend.instrumentation import contextual_logging + + contextual_logging.clear_context_metadata() + + with mock.patch("bugsnag.configure"), mock.patch("bugsnag.before_notify"): + bugsnag_module.setup(service_name="orchestrator") + + mock_event = mock.MagicMock() + mock_event.original_error = None + mock_event.metadata = {} + + bugsnag_module._before_notify(mock_event) + + mock_event.add_tab.assert_called_once_with("custom", {"slice_name": "orchestrator"}) + + def test_before_notify_skips_empty_context(monkeypatch): monkeypatch.setenv("TANGLE_BUGSNAG_API_KEY", "test-api-key") monkeypatch.setenv("TANGLE_ENV", "staging")