Skip to content

Commit b63fe80

Browse files
pavanputhraclaude
andauthored
feat(error_tracking): wire release + optional enrichment extension (#187)
Two small additions to ``init_sentry`` / ``init_error_tracker``: 1. Pass ``release=VCON_SERVER_GIT_COMMIT`` to ``sentry_sdk.init``. The build commit is already baked into the Docker image; surfacing it on every Sentry event lets a regression be tied back to a specific deploy. 2. After OSS init, opportunistically import an ``error_tracking_ext`` module and call its ``enrich()`` function. Lets a deployment attach process-wide Sentry tags / context without forking this file. The import is wrapped in ``try/except ImportError`` so the module is optional, and the call is wrapped in ``try/except Exception`` so a broken enrichment can never abort startup — it logs and continues. Tests cover release wiring (present + absent commit), the hook present, the hook absent, and a hook that raises. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4956d4b commit b63fe80

2 files changed

Lines changed: 123 additions & 4 deletions

File tree

‎common/lib/error_tracking.py‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import os
1+
import importlib
22
import logging
3+
import os
4+
35
import sentry_sdk
46
from sentry_sdk.integrations.logging import LoggingIntegration
57

@@ -8,6 +10,10 @@ def init_sentry():
810
sentry_sdk.init(
911
dsn=os.environ["SENTRY_DSN"],
1012
environment=os.environ["ENV"],
13+
# Tag every event with the build commit so a regression can be
14+
# bisected to a specific deploy. Unset releases are fine — Sentry
15+
# treats the field as optional.
16+
release=os.environ.get("VCON_SERVER_GIT_COMMIT") or None,
1117
integrations=[
1218
LoggingIntegration(
1319
level=logging.INFO, # Capture info and above as breadcrumbs
@@ -19,8 +25,25 @@ def init_sentry():
1925

2026

2127
def init_error_tracker():
22-
if os.environ.get("SENTRY_DSN"):
23-
init_sentry()
28+
if not os.environ.get("SENTRY_DSN"):
29+
return
30+
init_sentry()
31+
# Optional downstream enrichment hook. Deployments that ship an
32+
# ``error_tracking_ext`` module on the Python path can attach
33+
# process-wide Sentry tags / context there (e.g. proprietary
34+
# identifiers not appropriate for OSS). Imported opportunistically
35+
# so OSS keeps zero compile-time dependency on it; an enrichment
36+
# failure is caught so it never breaks startup.
37+
try:
38+
ext = importlib.import_module("error_tracking_ext")
39+
except ImportError:
40+
return
41+
try:
42+
ext.enrich()
43+
except Exception:
44+
logging.getLogger(__name__).exception(
45+
"error_tracking_ext.enrich() raised; continuing without enrichment"
46+
)
2447

2548

2649
def capture_exception(e):

‎common/tests/lib/test_error_tracking.py‎

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import sys
2+
import types
13
from unittest.mock import patch
24

3-
from lib.error_tracking import capture_exception, init_error_tracker
5+
import pytest
6+
7+
from lib.error_tracking import capture_exception, init_error_tracker, init_sentry
48

59

610
@patch("lib.error_tracking.init_sentry")
@@ -21,6 +25,98 @@ def test_init_error_tracker_initializes_when_dsn_is_present(mock_init_sentry, mo
2125
mock_init_sentry.assert_called_once()
2226

2327

28+
@pytest.fixture
29+
def _no_ext_module():
30+
# Tests that exercise init_error_tracker shouldn't see whatever
31+
# error_tracking_ext is on the host's path; clear it for the test.
32+
saved = sys.modules.pop("error_tracking_ext", None)
33+
try:
34+
yield
35+
finally:
36+
if saved is not None:
37+
sys.modules["error_tracking_ext"] = saved
38+
39+
40+
@patch("lib.error_tracking.sentry_sdk.init")
41+
def test_init_sentry_passes_release_from_git_commit_env(mock_init, monkeypatch):
42+
# Sentry's release field is what lets a regression be tied back to a
43+
# specific deploy. The Docker image already bakes in the commit SHA,
44+
# so wire it through if present.
45+
monkeypatch.setenv("SENTRY_DSN", "https://example@sentry.io/1")
46+
monkeypatch.setenv("ENV", "test")
47+
monkeypatch.setenv("VCON_SERVER_GIT_COMMIT", "abc1234")
48+
49+
init_sentry()
50+
51+
kwargs = mock_init.call_args.kwargs
52+
assert kwargs["release"] == "abc1234"
53+
54+
55+
@patch("lib.error_tracking.sentry_sdk.init")
56+
def test_init_sentry_release_is_none_when_commit_env_missing(mock_init, monkeypatch):
57+
# Release is optional; an unset commit shouldn't poison init with "".
58+
monkeypatch.setenv("SENTRY_DSN", "https://example@sentry.io/1")
59+
monkeypatch.setenv("ENV", "test")
60+
monkeypatch.delenv("VCON_SERVER_GIT_COMMIT", raising=False)
61+
62+
init_sentry()
63+
64+
assert mock_init.call_args.kwargs["release"] is None
65+
66+
67+
@patch("lib.error_tracking.init_sentry")
68+
def test_init_error_tracker_invokes_extension_enrich_when_present(
69+
mock_init_sentry, monkeypatch, _no_ext_module
70+
):
71+
# Deployments can ship an error_tracking_ext module to attach
72+
# proprietary tags after OSS init. When present, its enrich()
73+
# must be called once.
74+
monkeypatch.setenv("SENTRY_DSN", "https://example@sentry.io/1")
75+
fake_ext = types.ModuleType("error_tracking_ext")
76+
enrich_calls = []
77+
fake_ext.enrich = lambda: enrich_calls.append(1)
78+
sys.modules["error_tracking_ext"] = fake_ext
79+
80+
init_error_tracker()
81+
82+
mock_init_sentry.assert_called_once()
83+
assert enrich_calls == [1]
84+
85+
86+
@patch("lib.error_tracking.init_sentry")
87+
def test_init_error_tracker_tolerates_missing_extension_module(
88+
mock_init_sentry, monkeypatch, _no_ext_module
89+
):
90+
# The extension hook is opportunistic — absence is the OSS default,
91+
# not an error.
92+
monkeypatch.setenv("SENTRY_DSN", "https://example@sentry.io/1")
93+
94+
init_error_tracker() # must not raise
95+
96+
mock_init_sentry.assert_called_once()
97+
98+
99+
@patch("lib.error_tracking.init_sentry")
100+
def test_init_error_tracker_swallows_extension_errors(
101+
mock_init_sentry, monkeypatch, _no_ext_module, caplog
102+
):
103+
# A broken enrichment must never abort startup. Log and continue.
104+
monkeypatch.setenv("SENTRY_DSN", "https://example@sentry.io/1")
105+
fake_ext = types.ModuleType("error_tracking_ext")
106+
107+
def boom():
108+
raise RuntimeError("broken extension")
109+
110+
fake_ext.enrich = boom
111+
sys.modules["error_tracking_ext"] = fake_ext
112+
113+
with caplog.at_level("ERROR"):
114+
init_error_tracker()
115+
116+
mock_init_sentry.assert_called_once()
117+
assert any("error_tracking_ext.enrich() raised" in r.message for r in caplog.records)
118+
119+
24120
@patch("lib.error_tracking.sentry_sdk.capture_exception")
25121
def test_capture_exception_only_reports_when_enabled(mock_capture_exception, monkeypatch):
26122
err = RuntimeError("boom")

0 commit comments

Comments
 (0)