Skip to content

sample_rate=0.0 is silently replaced by 1.0, so a documented value cannot be set #1782

Description

@toolshedlabs-hash

Summary

Langfuse(sample_rate=0.0) traces everything. _client/client.py:354 is

sample_rate = sample_rate or float(os.environ.get(LANGFUSE_SAMPLE_RATE, 1.0))

and 0.0 or 1.0 is 1.0, so the value is replaced before it is used. The next line validates
0.0 <= sample_rate <= 1.0 and the docstring documents the range as "0.0 to 1.0", so 0.0 is an
accepted, documented value that never reaches the sampler.

Reproduction

langfuse 4.14.1. 40 observations per case, counting spans that arrive at a SpanExporter.

how the rate was set stored sample_rate sampler spans exported
sample_rate=0.0 keyword 1.0 ParentBased (the default) 40 of 40
sample_rate=0.5 keyword 0.5 TraceIdRatioBased 14 of 40
nothing passed 1.0 ParentBased (the default) 40 of 40
LANGFUSE_SAMPLE_RATE=0.0 0.0 TraceIdRatioBased 0 of 40

The keyword and the environment variable carry the same value and do opposite things. The
keyword path is also the one that does the opposite of what was asked for, which is the part
that would cost someone money.

Two things had to be true for those numbers to mean anything, and both caught me out first:
langfuse keeps one resource manager per public_key, and an OpenTelemetry global tracer
provider can only be registered once per process. Sharing either across cases silently reuses
the first client's pipeline. Each row above is a separate process building exactly one client.

Suggested fix

if sample_rate is None:
    sample_rate = float(os.environ.get(LANGFUSE_SAMPLE_RATE, 1.0))

timeout seven lines down reads the same way (timeout or int(os.environ.get(LANGFUSE_TIMEOUT, 5))), so a keyword 0 is unreachable there too. I do not know whether 0 means anything for a
timeout in your client, so I am pointing at the shape and not calling it a second bug.
flush_at, flush_interval and media_upload_thread_count are fine, they pass through as
Optional and are stored as given.

A separate one, if you want it

Three code examples in the shipped docstrings call a method that no longer exists. client.py:289,
which is the Langfuse class docstring and so the first thing help(Langfuse) prints, has

with span.start_as_current_generation(name="generate-response", model="gpt-4") as generation:

and client.py:1446 plus client.py:2147 have langfuse.start_as_current_generation(...). Run
verbatim on 4.14.1 those raise

AttributeError: 'LangfuseSpan' object has no attribute 'start_as_current_generation'
AttributeError: 'Langfuse' object has no attribute 'start_as_current_generation'

start_as_current_observation(name=..., as_type="generation") is the working form. Say the word
and I will open that separately instead of filing two at once.

How I found this, and what I am not claiming

I maintain a test harness that measures whether coding models can drive a given SDK, by running
the code they write and asserting on the HTTP the SDK emits. This is not that. langfuse has
never been through it, so I have no numbers about models and this is not a report about them. I
went looking at how the client resolves configuration and found the sampling line by reading,
then measured it.

toolshed is a small studio run by its owner, who directs the work, and AI does a lot of the
engineering.

Cal / toolshed / toolshedlabs@gmail.com

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions