feat(llmobs): sampling decisions, rates, and propagation#9030
Conversation
Compute a keep/drop decision + rate once on the root LLMObs span and inherit it across spans and services (via x-datadog-tags). Spans are always shipped; the decision is recorded in the event _dd block so the backend can honor it. Mirrors dd-trace-py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🎉 All green!🧪 All tests passed 🔗 Commit SHA: ff8b677 | Docs | Datadog PR Page | Give us feedback! |
Overall package sizeSelf size: 6.36 MB Dependency sizes| name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.2.0 | 104.26 kB | 843.44 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |🤖 This report was automatically generated by heaviest-objects-in-the-universe |
BenchmarksBenchmark execution time: 2026-06-26 17:06:40 Comparing candidate commit ff8b677 in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 1951 metrics, 14 unstable metrics.
|
- Match dd-trace-py: only the true trace root makes a fresh sampling decision; a span under a propagated LLMObs parent inherits whatever was propagated (possibly none) instead of starting a divergent one. - Reuse the shared formatKnuthRate (hoisted to sampler.js) instead of a bespoke llmobs formatRate. - Rename handleLLMObsParentIdInjection -> handleLLMObsInjection. - Declare llmobs.sampleRate in the public index.d.ts (fixes config-names lint) and tag sample_rate on enablement telemetry. - Drop redundant comments flagged in review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…kim/llmobs-sampling
- Rebuild the sampler lazily from the live config rate so a disable/ re-enable (or any rate change) is reflected, instead of caching one sampler at construction. - Tweak the injection comment wording. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep the formatter in priority_sampler.js (export it) instead of hoisting to sampler.js, per review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sample rate is set at init and not mutable at runtime, so the per-span lazy rebuild was unnecessary work on a hot path. Build it once at construction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dbda2e5b15
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…onfig-reactive - Mirror `llmobs.sampleRate` in the v5 type declarations (index.d.v5.ts), per the repo type policy requiring non-v6-only public APIs in both declaration surfaces (PR review). - Register DD_LLMOBS_SAMPLE_RATE as implementation "A" in supported-configurations.json (PR review); generated types are unchanged. - Build the LLMObs sampler lazily and rebuild it when config.llmobs.sampleRate changes, instead of fixing it for the tagger's lifetime, so a future remote config update to the sample rate takes effect without re-instantiation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sabrenner
left a comment
There was a problem hiding this comment.
just a couple of comments but lgtm!
- Drop the redundant #samplerRate field; use sampler.rate() for the rebuild check.
- Move formatKnuthRate to the shared src/util.js so the LLMObs tagger no longer
imports priority_sampler just for a string formatter.
- Reword the sampleRate JSDoc in index.d.ts and index.d.v5.ts ("honored at
ingestion time").
- Test rates that exercise the formatter: 1/3 (6-decimal cap) and 0.25
(trailing-zero stripping).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(llmobs): add sampling decisions, rates, and propagation
Compute a keep/drop decision + rate once on the root LLMObs span and
inherit it across spans and services (via x-datadog-tags). Spans are
always shipped; the decision is recorded in the event _dd block so the
backend can honor it. Mirrors dd-trace-py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(llmobs): address sampling PR review comments
- Match dd-trace-py: only the true trace root makes a fresh sampling
decision; a span under a propagated LLMObs parent inherits whatever
was propagated (possibly none) instead of starting a divergent one.
- Reuse the shared formatKnuthRate (hoisted to sampler.js) instead of a
bespoke llmobs formatRate.
- Rename handleLLMObsParentIdInjection -> handleLLMObsInjection.
- Declare llmobs.sampleRate in the public index.d.ts (fixes config-names
lint) and tag sample_rate on enablement telemetry.
- Drop redundant comments flagged in review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(llmobs): regenerate config types after merging master
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(llmobs): address follow-up sampling review comments
- Rebuild the sampler lazily from the live config rate so a disable/
re-enable (or any rate change) is reflected, instead of caching one
sampler at construction.
- Tweak the injection comment wording.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(llmobs): export formatKnuthRate from priority_sampler.js
Keep the formatter in priority_sampler.js (export it) instead of
hoisting to sampler.js, per review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(llmobs): build the sampler once in the tagger constructor
The sample rate is set at init and not mutable at runtime, so the
per-span lazy rebuild was unnecessary work on a hot path. Build it once
at construction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Update packages/dd-trace/src/llmobs/tagger.js
* refactor(llmobs): address sampling review comments and make sampler config-reactive
- Mirror `llmobs.sampleRate` in the v5 type declarations (index.d.v5.ts), per
the repo type policy requiring non-v6-only public APIs in both declaration
surfaces (PR review).
- Register DD_LLMOBS_SAMPLE_RATE as implementation "A" in
supported-configurations.json (PR review); generated types are unchanged.
- Build the LLMObs sampler lazily and rebuild it when config.llmobs.sampleRate
changes, instead of fixing it for the tagger's lifetime, so a future remote
config update to the sample rate takes effect without re-instantiation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(llmobs): match DD_LLMOBS_SAMPLE_RATE default to config registry (1.0)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(llmobs): address remaining sampling review comments
- Drop the redundant #samplerRate field; use sampler.rate() for the rebuild check.
- Move formatKnuthRate to the shared src/util.js so the LLMObs tagger no longer
imports priority_sampler just for a string formatter.
- Reword the sampleRate JSDoc in index.d.ts and index.d.v5.ts ("honored at
ingestion time").
- Test rates that exercise the formatter: 1/3 (6-decimal cap) and 0.25
(trailing-zero stripping).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(llmobs): add sampling decisions, rates, and propagation
Compute a keep/drop decision + rate once on the root LLMObs span and
inherit it across spans and services (via x-datadog-tags). Spans are
always shipped; the decision is recorded in the event _dd block so the
backend can honor it. Mirrors dd-trace-py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(llmobs): address sampling PR review comments
- Match dd-trace-py: only the true trace root makes a fresh sampling
decision; a span under a propagated LLMObs parent inherits whatever
was propagated (possibly none) instead of starting a divergent one.
- Reuse the shared formatKnuthRate (hoisted to sampler.js) instead of a
bespoke llmobs formatRate.
- Rename handleLLMObsParentIdInjection -> handleLLMObsInjection.
- Declare llmobs.sampleRate in the public index.d.ts (fixes config-names
lint) and tag sample_rate on enablement telemetry.
- Drop redundant comments flagged in review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(llmobs): regenerate config types after merging master
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(llmobs): address follow-up sampling review comments
- Rebuild the sampler lazily from the live config rate so a disable/
re-enable (or any rate change) is reflected, instead of caching one
sampler at construction.
- Tweak the injection comment wording.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(llmobs): export formatKnuthRate from priority_sampler.js
Keep the formatter in priority_sampler.js (export it) instead of
hoisting to sampler.js, per review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(llmobs): build the sampler once in the tagger constructor
The sample rate is set at init and not mutable at runtime, so the
per-span lazy rebuild was unnecessary work on a hot path. Build it once
at construction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Update packages/dd-trace/src/llmobs/tagger.js
* refactor(llmobs): address sampling review comments and make sampler config-reactive
- Mirror `llmobs.sampleRate` in the v5 type declarations (index.d.v5.ts), per
the repo type policy requiring non-v6-only public APIs in both declaration
surfaces (PR review).
- Register DD_LLMOBS_SAMPLE_RATE as implementation "A" in
supported-configurations.json (PR review); generated types are unchanged.
- Build the LLMObs sampler lazily and rebuild it when config.llmobs.sampleRate
changes, instead of fixing it for the tagger's lifetime, so a future remote
config update to the sample rate takes effect without re-instantiation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(llmobs): match DD_LLMOBS_SAMPLE_RATE default to config registry (1.0)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(llmobs): address remaining sampling review comments
- Drop the redundant #samplerRate field; use sampler.rate() for the rebuild check.
- Move formatKnuthRate to the shared src/util.js so the LLMObs tagger no longer
imports priority_sampler just for a string formatter.
- Reword the sampleRate JSDoc in index.d.ts and index.d.v5.ts ("honored at
ingestion time").
- Test rates that exercise the formatter: 1/3 (6-decimal cap) and 0.25
(trailing-zero stripping).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sampling tests in #9030 build their own taggers with `{ llmobs: { enabled: true } }`, and #8943 renamed that config key to `DD_LLMOBS_ENABLED` everywhere it could see. The two landed in parallel, so #8943 normalized the rest of the file but never saw these four fixtures. On master the tagger now reads `DD_LLMOBS_ENABLED`, finds it undefined, and returns before registering the span; `Tagger.tagMap.get` then yields undefined and the "DROPPED at sampleRate 0" test throws synchronously, aborting the whole `test:llmobs:sdk:ci` run with exit 7. Fixes: https://github.com/DataDog/dd-trace-js/actions/runs/28265509637/job/83751636644
The sampling tests in #9030 build their own taggers with `{ llmobs: { enabled: true } }`, and #8943 renamed that config key to `DD_LLMOBS_ENABLED` everywhere it could see. The two landed in parallel, so #8943 normalized the rest of the file but never saw these four fixtures. On master the tagger now reads `DD_LLMOBS_ENABLED`, finds it undefined, and returns before registering the span; `Tagger.tagMap.get` then yields undefined and the "DROPPED at sampleRate 0" test throws synchronously, aborting the whole `test:llmobs:sdk:ci` run with exit 7. Fixes: https://github.com/DataDog/dd-trace-js/actions/runs/28265509637/job/83751636644
Make and record LLMO sampling decisions on LLMO spans (always ship, we drop in the backend). Allow users to set via DD_LLMOBS_SAMPLE_RATE or via
tracer.init({ llmobs: {sampleRate: ...} })x-datadog-tags(_dd.p.llmobs_sr/_dd.p.llmobs_sd).Claude session:
1688bbe5-066d-47dc-ae28-8678aef20ee2Resume:
claude --resume 1688bbe5-066d-47dc-ae28-8678aef20ee2🤖 Generated with Claude Code