Skip to content

Commit fe734e7

Browse files
turnipdabeetsclaude
andcommitted
feat(traces): before_span_send hook
Adds the traces `before_span_send` option: a callable, or a list run in order, that receives each finished span as a plain dict (like the events before_send hook) and returns it edited, or None to drop it. It is the documented place to scrub sensitive values, so a hook that raises drops the span rather than exporting it unscrubbed. trace_id, span_id and parent_span_id are read-only; names, times, status and events are re-sanitized and the per-span limits re-applied to whatever the hook returns. The hook runs with no tracing lock held, and entries that are not callable are ignored with a warning. Not reachable from the client. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkZAsCciW4PV8ZdcCHmAbA
1 parent 8de0208 commit fe734e7

8 files changed

Lines changed: 723 additions & 6 deletions

File tree

‎posthog/test/tracing/test_config.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,52 @@ def test_an_unusable_value_falls_back_rather_than_dropping_every_span(self, valu
221221
assert resolved.max_attributes_per_span == DEFAULT_MAX_ATTRIBUTES_PER_SPAN
222222
assert resolved.max_events_per_span == DEFAULT_MAX_EVENTS_PER_SPAN
223223
assert resolved.max_attribute_value_length == DEFAULT_MAX_ATTRIBUTE_VALUE_LENGTH
224+
225+
226+
class TestBeforeSpanSendConfig:
227+
def test_accepts_one_hook_or_a_list(self):
228+
def hook(span):
229+
return span
230+
231+
assert resolve_traces_config({"before_span_send": hook}).before_span_send == (
232+
hook,
233+
)
234+
assert resolve_traces_config(
235+
{"before_span_send": [hook, hook]}
236+
).before_span_send == (hook, hook)
237+
238+
def test_defaults_to_no_hooks(self):
239+
assert resolve_traces_config({}).before_span_send == ()
240+
241+
def test_skips_falsy_entries_silently(self, caplog):
242+
caplog.set_level("WARNING", logger="posthog")
243+
244+
def hook(span):
245+
return span
246+
247+
resolved = resolve_traces_config({"before_span_send": [None, False, hook]})
248+
assert resolved.before_span_send == (hook,)
249+
assert not caplog.records
250+
251+
def test_ignores_and_warns_about_entries_that_are_not_callable(self, caplog):
252+
caplog.set_level("WARNING", logger="posthog")
253+
254+
def hook(span):
255+
return span
256+
257+
resolved = resolve_traces_config({"before_span_send": ["scrub", hook]})
258+
assert resolved.before_span_send == (hook,)
259+
assert any("1 of 2" in r.getMessage() for r in caplog.records)
260+
261+
def test_a_hook_whose_truthiness_raises_is_still_resolved(self):
262+
class Hook:
263+
def __bool__(self):
264+
raise RuntimeError("no")
265+
266+
def __call__(self, span):
267+
return span
268+
269+
hook = Hook()
270+
assert resolve_traces_config({"before_span_send": hook}).before_span_send == (
271+
hook,
272+
)

0 commit comments

Comments
 (0)