The pipeline
Each event runs through these steps, in order, before it leaves the process:
event construction → scrubber → before_send → sampling → enqueue- scrubber —
scrubber(event_dict) -> event_dict. Redact or strip fields. Runs first, on every event. - before_send —
before_send(event_dict) -> event_dict | None. Mutate an event, or returnNoneto drop it. - sampling — keep or drop the whole trace (see below).
Scrubber
The scrubber runs first, on every event, and must return an event dict. Use it to remove fields you never want to leave the process.
def scrub(event: dict) -> dict: event.get("context", {}).pop("user_traits", None) return eventclient = Rollout(api_key="...", scrubber=scrub, send_default_pii=False)before_send
before_send runs after the scrubber. Return the (possibly mutated) event to keep it, or return None to drop it entirely. This is the place for conditional sampling, tagging, or last-mile redaction.
def before_send(event: dict) -> dict | None: if event.get("event_type") == "message" and is_internal_debug(event): return None # drop this event event.setdefault("attributes", {})["pipeline"] = "v2" return eventclient = Rollout(api_key="...", before_send=before_send)PII
send_default_pii defaults to False. The SDK never requires an email or a name. If you explicitly attach PII — for example via user traits — it still passes through the scrubber before it is queued, so the scrubber is your single chokepoint for controlling what leaves the process.
Heads up
Because the scrubber and before_send run in-process before anything is enqueued, data you strip there is never transmitted — not merely hidden in the dashboard.
Sampling
Sampling is per trace: once a trace is sampled in, all of its spans and events are kept, so you never get partially-sampled traces. Set the rate with sample_rate (or ROLLOUT_SAMPLE_RATE) — see Configuration.