Skip to content

Commit fb44029

Browse files
fix(sdk): flip content_capture_rate default 0.0 → 0.10 (Codex 2026-04-24 P0 #1)
Pre-change, the default install silently produced no judgeable content: set_input_content / set_output_content / set_retrieval_content no-op'd because their rate-gate evaluated ``random.random() < 0.0 == False``. The L2 evaluator judge then returned empty test cases for every span and eval_rollups stayed ``pending`` forever. Customers had to read the docs AND flip a config flag before the "SDK in → eval out" loop worked. Flipping the default to 0.10 (what the docs already recommended for production) inverts the failure mode from silent-no-eval to loud- content-captured-with-PII-risk. Three PII-defense layers still run on every captured attribute: 1. SDK in-process scrub (pii_scrub_enabled=True by default) 2. Collector credential-regex scrub on content attribute prefixes 3. Evaluator Presidio NER before judge calls Changes: - BotanuConfig.content_capture_rate default 0.0 → 0.10 - YAML loader fallback default matches (config.py:436) - Bootstrap logs content_capture_rate at every startup; warns when <= 0.0 so the "why are eval rollups empty" debugging path ends at a startup log line, not silently-dropped helper calls. - Tests: test_default_is_zero → test_default_is_ten_percent, test_env_var_invalid_ignored now expects 0.10 fallback. - Docs: api/configuration.md table + getting-started/configuration.md code sample both updated. All 150 SDK unit tests still pass. NOTE: working-tree had one unrelated pre-existing change to src/botanu/models/run_context.py — NOT included in this commit. Signed-off-by: Deborah Jacob <deborah@botanu.ai>
1 parent 122777a commit fb44029

5 files changed

Lines changed: 45 additions & 16 deletions

File tree

docs/api/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ from botanu.sdk.config import BotanuConfig
1919
| `auto_detect_resources` | `bool` | `True` | Auto-detect cloud resources |
2020
| `otlp_endpoint` | `str` | From env / auto-configured when `BOTANU_API_KEY` is set / `"http://localhost:4318"` | OTLP endpoint |
2121
| `otlp_headers` | `dict` | `None` | Custom headers for OTLP exporter — always honored |
22-
| `content_capture_rate` | `float` | `0.0` | Prompt/response capture rate (0.0–1.0). See [Content Capture](../tracking/content-capture.md). |
22+
| `content_capture_rate` | `float` | `0.10` | Prompt/response capture rate (0.0–1.0). Default 10% sample. See [Content Capture](../tracking/content-capture.md). |
2323
| `pii_scrub_enabled` | `bool` | `True` | In-process PII scrub of captured content |
2424
| `pii_scrub_use_presidio` | `bool` | `False` | Add Microsoft Presidio NER to the scrub pipeline |
2525
| `max_export_batch_size` | `int` | `512` | Max spans per batch |

docs/getting-started/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class BotanuConfig:
8080
schedule_delay_millis: int = 5000
8181
export_timeout_millis: int = 30000
8282

83-
content_capture_rate: float = 0.0
83+
content_capture_rate: float = 0.10
8484
```
8585

8686
`BOTANU_API_KEY` is not a field on the dataclass — when the env var is set, `BotanuConfig` auto-configures `otlp_endpoint` + `otlp_headers` for the botanu-trusted endpoint.

src/botanu/sdk/bootstrap.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,22 @@ def enable(
163163
from botanu.sdk.config import _redact_url_credentials
164164

165165
logger.info(
166-
"Initializing Botanu SDK: service=%s, env=%s, endpoint=%s",
166+
"Initializing Botanu SDK: service=%s, env=%s, endpoint=%s, content_capture_rate=%s",
167167
cfg.service_name,
168168
cfg.deployment_environment,
169169
_redact_url_credentials(traces_endpoint),
170+
cfg.content_capture_rate,
170171
)
172+
if cfg.content_capture_rate <= 0.0:
173+
# Louder signal when the customer explicitly turned capture off —
174+
# evaluator judge will no-op on every span. Not a failure, just a
175+
# disable-by-choice worth flagging once at startup so a bug hunt
176+
# for "why are eval rollups empty" ends here (Codex 2026-04-24 P0 #1).
177+
logger.warning(
178+
"Botanu content_capture_rate=0.0 — set_input_content / set_output_content "
179+
"will not write span attributes, and the L2 evaluator judge will return "
180+
"no test case for any span. See docs/tracking/content-capture.md to enable."
181+
)
171182

172183
try:
173184
from opentelemetry import trace

src/botanu/sdk/config.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,23 @@ class BotanuConfig:
126126
schedule_delay_millis: int = 5000
127127
export_timeout_millis: int = 30000
128128

129-
# Content capture for eval — 0.0 disables entirely (default, privacy-safe).
130-
# Set to 1.0 for sandbox/shadow, 0.10-0.20 for production. Customers must also
131-
# call set_input_content() / set_output_content() on their trackers; this rate
132-
# gates whether those calls actually write to span attributes. In-process PII
133-
# scrubbing runs on the captured text before it hits the span (see
134-
# pii_scrub_* fields below); collector regex + evaluator Presidio NER
135-
# remain belt-and-suspenders.
136-
content_capture_rate: float = 0.0
129+
# Content capture for eval — 0.10 default (~10% sample). Pre-2026-04-24
130+
# this defaulted to 0.0, which meant the default install silently
131+
# produced no judgeable content: set_input_content / set_output_content
132+
# no-op'd, the L2 judge returned empty test cases, and eval rollups
133+
# stayed pending forever. Customers had to read the docs AND flip a
134+
# config flag before the "SDK in, eval out" loop worked at all.
135+
#
136+
# 0.10 is what the configuration docs recommend for production; it's
137+
# enough traffic for the judge to have real cases while keeping PII
138+
# exposure + storage cost bounded. Three PII-defense layers still
139+
# fire on every captured attribute:
140+
# 1. SDK in-process scrub (pii_scrub_enabled — default True below)
141+
# 2. Collector credential-regex scrub on content attribute prefixes
142+
# 3. Evaluator Presidio NER pass before judge calls
143+
# Set to 1.0 for sandbox/shadow, 0.0 to fully disable (e.g. HIPAA
144+
# where any content capture is a legal hazard).
145+
content_capture_rate: float = 0.10
137146

138147
# In-process PII scrubbing — runs on text passed to set_input_content /
139148
# set_output_content / set_retrieval_content before the span attribute is
@@ -424,7 +433,7 @@ def _from_dict(
424433
max_queue_size=export.get("queue_size", 65536),
425434
schedule_delay_millis=export.get("delay_ms", 5000),
426435
export_timeout_millis=export.get("export_timeout_ms", 30000),
427-
content_capture_rate=max(0.0, min(1.0, float(eval_cfg.get("content_capture_rate", 0.0)))),
436+
content_capture_rate=max(0.0, min(1.0, float(eval_cfg.get("content_capture_rate", 0.10)))),
428437
pii_scrub_enabled=bool(pii_cfg.get("enabled", True)),
429438
pii_scrub_disable_patterns=pii_cfg.get("disable_patterns"),
430439
pii_scrub_custom_patterns=pii_cfg.get("custom_patterns"),

tests/unit/test_config.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -454,12 +454,20 @@ def test_default_packages(self):
454454
class TestContentCaptureRate:
455455
"""Tests for the content_capture_rate field."""
456456

457-
def test_default_is_zero(self):
458-
"""Privacy-safe default: no content captured unless explicitly enabled."""
457+
def test_default_is_ten_percent(self):
458+
"""Default changed from 0.0 to 0.10 on 2026-04-24 (Codex P0 #1).
459+
460+
Pre-change: SDK worked out of the box except the evaluator judge
461+
silently returned no test case for every span because the helper
462+
methods (set_input_content / set_output_content) gated on rate > 0.0.
463+
0.10 matches the docs' recommended production value; three PII-defense
464+
layers (SDK scrub + collector credential scrub + Presidio NER) still
465+
fire on every captured attribute.
466+
"""
459467
with mock.patch.dict(os.environ, {}, clear=True):
460468
os.environ.pop("BOTANU_CONTENT_CAPTURE_RATE", None)
461469
config = BotanuConfig()
462-
assert config.content_capture_rate == 0.0
470+
assert config.content_capture_rate == 0.10
463471

464472
def test_explicit_value_respected(self):
465473
config = BotanuConfig(content_capture_rate=0.15)
@@ -486,7 +494,8 @@ def test_env_var_invalid_ignored(self):
486494
"""Invalid env values are ignored (default retained)."""
487495
with mock.patch.dict(os.environ, {"BOTANU_CONTENT_CAPTURE_RATE": "not_a_number"}):
488496
config = BotanuConfig()
489-
assert config.content_capture_rate == 0.0
497+
# Default since 2026-04-24 is 0.10, not 0.0.
498+
assert config.content_capture_rate == 0.10
490499

491500
def test_to_dict_roundtrip(self):
492501
config = BotanuConfig(content_capture_rate=0.1)

0 commit comments

Comments
 (0)