Skip to content

Commit 3bd7f4b

Browse files
committed
fixup
1 parent dcb7484 commit 3bd7f4b

3 files changed

Lines changed: 46 additions & 11 deletions

File tree

py/src/braintrust/logger.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2478,6 +2478,22 @@ def _braintrust_parent_to_components(braintrust_parent: str):
24782478
return None
24792479

24802480

2481+
def _set_header(carrier: dict, name: str, value: str) -> None:
2482+
"""Set a W3C trace-context header on a carrier, sending the lowercase name.
2483+
2484+
Per the W3C Trace Context spec (§3.2.1 / §3.3.1), vendors SHOULD send these
2485+
header names in lowercase. ``name`` is always the canonical lowercase key.
2486+
A plain ``dict`` carrier is case-sensitive, so any pre-existing case-variant
2487+
(e.g. ``Baggage`` from a framework that title-cases headers) must be removed
2488+
first, otherwise the carrier would end up with two conflicting headers.
2489+
"""
2490+
lowered = name.lower()
2491+
for key in list(carrier.keys()):
2492+
if isinstance(key, str) and key != name and key.lower() == lowered:
2493+
del carrier[key]
2494+
carrier[name] = value
2495+
2496+
24812497
def _inject_into_carrier(
24822498
carrier: dict,
24832499
trace_id: str,
@@ -2496,11 +2512,11 @@ def _inject_into_carrier(
24962512
if traceparent is None:
24972513
# Ids aren't W3C-shaped (e.g. legacy UUID mode); nothing to propagate.
24982514
return
2499-
carrier[TRACEPARENT_HEADER] = traceparent
2515+
_set_header(carrier, TRACEPARENT_HEADER, traceparent)
25002516

25012517
# Forward upstream tracestate (per W3C, only alongside a valid traceparent).
25022518
if tracestate:
2503-
carrier[TRACESTATE_HEADER] = tracestate
2519+
_set_header(carrier, TRACESTATE_HEADER, tracestate)
25042520

25052521
# Merge braintrust.parent into any existing baggage, preserving other keys.
25062522
existing = get_header(carrier, BAGGAGE_HEADER)
@@ -2509,7 +2525,7 @@ def _inject_into_carrier(
25092525
entries[BRAINTRUST_PARENT_KEY] = braintrust_parent
25102526
baggage_value = format_baggage(entries)
25112527
if baggage_value is not None:
2512-
carrier[BAGGAGE_HEADER] = baggage_value
2528+
_set_header(carrier, BAGGAGE_HEADER, baggage_value)
25132529

25142530

25152531
def inject_trace_context(carrier: dict | None = None, span: "Span | None" = None) -> dict:

py/src/braintrust/propagation.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,16 @@
88
Trace identity (trace id + parent span id) is carried in ``traceparent``; the
99
Braintrust container the trace belongs to (project/experiment) is carried in
1010
``baggage`` under the ``braintrust.parent`` key.
11-
12-
The deprecated ``x-bt-parent`` header (a serialized span slug) is accepted on
13-
receive for backwards compatibility with older native SDKs, but is never
14-
emitted -- ``traceparent`` takes priority whenever both are present.
1511
"""
1612

1713
import logging
1814
import re
1915

20-
from .http_headers import BT_PARENT
21-
2216

2317
__all__ = [
2418
"TRACEPARENT_HEADER",
2519
"TRACESTATE_HEADER",
2620
"BAGGAGE_HEADER",
27-
"BT_PARENT_HEADER",
2821
"BRAINTRUST_PARENT_KEY",
2922
"parse_traceparent",
3023
"format_traceparent",
@@ -38,7 +31,6 @@
3831
TRACEPARENT_HEADER = "traceparent"
3932
TRACESTATE_HEADER = "tracestate"
4033
BAGGAGE_HEADER = "baggage"
41-
BT_PARENT_HEADER = BT_PARENT # "x-bt-parent"
4234
BRAINTRUST_PARENT_KEY = "braintrust.parent"
4335

4436
# W3C traceparent: version-traceid-parentid-flags, version 00, lowercase hex.

py/src/braintrust/test_propagation.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,33 @@ def test_preexisting_baggage_preserved(self, memory_and_logger):
154154
assert parsed["team"] == "eng"
155155
assert parsed[BRAINTRUST_PARENT_KEY] == "project_name:propagation-test"
156156

157+
def test_title_cased_baggage_emits_single_lowercase_header(self, memory_and_logger):
158+
# Per W3C (§3.3.1) the header name SHOULD be sent lowercase. A carrier
159+
# that arrives with a title-cased `Baggage` (e.g. from a framework that
160+
# normalizes header casing) must be rewritten to a single lowercase
161+
# `baggage` key, not left with two conflicting case-variants.
162+
_mem, logger = memory_and_logger
163+
with logger.start_span(name="svc_a") as span:
164+
carrier = span.inject({"Baggage": "user=alice"})
165+
166+
baggage_keys = [k for k in carrier if k.lower() == BAGGAGE_HEADER]
167+
assert baggage_keys == [BAGGAGE_HEADER]
168+
parsed = parse_baggage(carrier[BAGGAGE_HEADER])
169+
assert parsed["user"] == "alice"
170+
assert parsed[BRAINTRUST_PARENT_KEY] == "project_name:propagation-test"
171+
172+
def test_title_cased_traceparent_emits_single_lowercase_header(self, memory_and_logger):
173+
# A pre-existing title-cased `Traceparent` must be replaced by a single
174+
# lowercase `traceparent` (W3C §3.2.1), with no stale variant remaining.
175+
_mem, logger = memory_and_logger
176+
with logger.start_span(name="svc_a") as span:
177+
carrier = span.inject({"Traceparent": "stale"})
178+
179+
traceparent_keys = [k for k in carrier if k.lower() == TRACEPARENT_HEADER]
180+
assert traceparent_keys == [TRACEPARENT_HEADER]
181+
tp = parse_traceparent(carrier[TRACEPARENT_HEADER])
182+
assert tp == (span.root_span_id, span.span_id)
183+
157184
def test_never_emits_x_bt_parent(self, memory_and_logger):
158185
_mem, logger = memory_and_logger
159186
with logger.start_span(name="svc_a") as span:

0 commit comments

Comments
 (0)