From 58fcde1e0039b405d8426a57925f5c0c3dd855fc Mon Sep 17 00:00:00 2001 From: JS Date: Wed, 21 Oct 2026 11:03:41 +0800 Subject: [PATCH] feat(tracing): forward the journey name alongside the journey id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the host stashes a journey name (flask.g.journey_name — set by flask_campus's @journey decorator or by the journeys middleware adopting X-Journey-Name, campus#952), SDK calls forward it as X-Journey-Name next to X-Journey-ID, so child services' spans tag the same journey_name and journey cards show the label end to end. Refs nyjc-computing/campus#952, campus-api-python#114 (the name will also ride on the process journey once #114 lands). --- campus_python/tracing.py | 14 +++++++-- tests/unit/test_trace_propagation.py | 46 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/campus_python/tracing.py b/campus_python/tracing.py index d55f915..e9398cb 100644 --- a/campus_python/tracing.py +++ b/campus_python/tracing.py @@ -33,6 +33,12 @@ # action journey. Keep the two in lockstep. JOURNEY_ID_HEADER = "X-Journey-ID" +# Mirror of campus.audit.middleware.journeys.ACTION_JOURNEY_NAME_HEADER +# (campus#952): forwarded with the id so a child service's spans tag +# the same human label (tags.journey_name) and journey cards show it +# end to end. Keep the two in lockstep. +JOURNEY_NAME_HEADER = "X-Journey-Name" + # Mirror of campus.config.DEVICE_ID_HEADER (campus#837): forwarded so a # child service's span carries the caller's device identity when the # host stashed one (flask_campus's push_context sets g.device from the @@ -75,10 +81,11 @@ def propagation_headers() -> dict[str, str]: tracing middleware turns the trace headers into a child span of the caller's span (#794, campus#816); the journeys middleware adopts X-Journey-ID so the child span joins the caller's action journey - (campus#828); the tracing middleware adopts X-Campus-Device for the + (campus#828), with X-Journey-Name carrying its human label + (campus#952); the tracing middleware adopts X-Campus-Device for the child span's device tag when it knows nothing better (campus#837). The three are independent: trace headers require span state on - flask.g (tracing middleware ran), the journey header only requires + flask.g (tracing middleware ran), the journey headers only require an active journey, the device header only requires a stashed g.device. """ @@ -97,6 +104,9 @@ def propagation_headers() -> dict[str, str]: journey_id = getattr(flask.g, "journey_id", None) if journey_id: headers[JOURNEY_ID_HEADER] = journey_id + journey_name = getattr(flask.g, "journey_name", None) + if journey_name: + headers[JOURNEY_NAME_HEADER] = journey_name device_id = getattr(flask.g, "device", None) if device_id: headers[DEVICE_ID_HEADER] = str(device_id) diff --git a/tests/unit/test_trace_propagation.py b/tests/unit/test_trace_propagation.py index 81500c0..5937300 100644 --- a/tests/unit/test_trace_propagation.py +++ b/tests/unit/test_trace_propagation.py @@ -181,6 +181,52 @@ def test_journey_header_outside_trace_context(self): self.assertNotIn("X-Request-ID", captured["headers"]) +class TestJourneyNameForwarding(unittest.TestCase): + """Journey-label forwarding on SDK calls (campus#952). + + When the host stashes a journey name (flask.g.journey_name — set by + flask_campus's @journey decorator or by the journeys middleware + adopting X-Journey-Name), SDK calls forward it alongside the id so + child services' spans tag the same journey_name. + """ + + def test_name_forwarded_with_journey_id(self): + app = flask.Flask(__name__) + client = _make_client() + captured = _capture_send(client) + + with app.test_request_context("/"): + flask.g.journey_id = "uid-journey-abc123" + flask.g.journey_name = "campus.submissions.submit" + client.get("/ping") + + self.assertEqual( + captured["headers"].get("X-Journey-Name"), + "campus.submissions.submit", + ) + self.assertEqual( + captured["headers"].get("X-Journey-ID"), "uid-journey-abc123" + ) + + def test_no_name_no_header(self): + """A journey without a stashed name emits no name header.""" + app = flask.Flask(__name__) + client = _make_client() + captured = _capture_send(client) + + with app.test_request_context("/"): + flask.g.journey_id = "uid-journey-abc123" + client.get("/ping") + + self.assertNotIn("X-Journey-Name", captured["headers"]) + + def test_name_lockstep_with_campus_middleware(self): + """The header name must match the campus journeys middleware's + adoption constant — the two modules carry keep-in-lockstep + notes and the whole chain breaks silently if they drift.""" + self.assertEqual(tracing.JOURNEY_NAME_HEADER, "X-Journey-Name") + + class TestDeviceForwarding(unittest.TestCase): """Device forwarding on SDK calls (campus#837).