Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions campus_python/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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)
Expand Down
46 changes: 46 additions & 0 deletions tests/unit/test_trace_propagation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
Loading