Skip to content

[OPIK-8040] [BE] fix: apply online evaluation sampling to production traces only - #7957

Merged
jverre merged 8 commits into
mainfrom
jacques/OPIK-8040-sampling-production-only
Aug 25, 2026
Merged

jverre merged 8 commits into
mainfrom
jacques/OPIK-8040-sampling-production-only

Conversation

@jverre

@jverre jverre commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Details

An online evaluation rule holds one sampling rate, and it applied to every trace the rule saw. A user who wanted to sample production traffic but score every experiment run could not do it, so they had to keep two copies of the same rule. This makes the sampling rate a production-only setting: OnlineScoringSampler now skips the sampling roll for any trace whose source is not SDK, so experiment, playground and optimization traces are always scored in full.

  • The bypass sits after the enabled and filter checks, so a disabled rule and its filters still apply to experiment traces — only the random roll is skipped.
  • Each bypassed trace gets one line on the rule's user-facing log stream saying the rate applies to production traces only, so a user can see why every run was scored.
  • No change was needed in the span sampler or the thread sampler: both already keep SDK-source entities only, so no experiment span or thread was ever sampled.

Only the sampling half of the ticket is in this PR. The other suggested improvement — built-in {{trace.output}} / {{trace.input}} / {{trace.metadata}} placeholders — is not addressed here.

Change checklist

  • User facing
  • Documentation update

Issues

  • OPIK-8040

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 5
  • Scope: whole change — backend logic, unit tests, API schema descriptions, docs line
  • Human verification: pending review by the author

Testing

Commands run in apps/opik-backend:

  • mvn test -Dtest=OnlineScoringSamplerTest — 50 tests, 0 failures.
  • mvn test -Dtest='OnlineScoringSpanSamplerTest,TraceThreadOnlineScoringSamplerListenerTest,AutomationRuleEvaluatorFiltersDeserializerTest' — 37 tests, 0 failures. These cover the two samplers the change deliberately leaves alone.
  • mvn spotless:apply then mvn spotless:check — clean.

Scenarios added to OnlineScoringSamplerTest.SamplingRateTests:

  • An experiment trace is scored at sampling rate 0.0, for both the LLM-as-a-judge and the Python evaluator.
  • A batch of one SDK trace and one experiment trace at rate 0.0 enqueues the experiment trace only.
  • A playground, optimization or evaluator trace carrying selected_rule_ids is scored at rate 0.0 (parameterized over the three sources).
  • A disabled rule still scores nothing on an experiment trace.
  • A rule whose filters do not match still scores nothing on an experiment trace.

Not run: the backend integration and E2E suites, which need MySQL, ClickHouse and Redis. The behaviour under test is pure in-process sampler logic and is covered by the unit tests above.

Documentation

  • Rewrote the sampling-rate bullet on the online evaluation rules page to say it covers production traces, and that experiment and playground traces are always scored in full.
  • Added a @Schema description on samplingRate in AutomationRuleEvaluator and AutomationRuleEvaluatorUpdate, so the generated OpenAPI spec and the SDKs carry the same statement. The committed openapi/opik.yaml is regenerated by the sdks_generate_openapi_spec_and_fern_code workflow, so it is not touched here.
  • The v1 docs version keeps the old wording. It does not document the rule trigger scope at all, so it already predates the production/experiment split.

🤖 Generated with Claude Code

…traces only

The sampling rate thins a continuous production stream, so it now applies to
SDK-logged traces only. Experiment, playground and optimization traces are runs
the user started one by one, so they bypass the sampling roll and are always
scored in full.

- OnlineScoringSampler skips the roll when the trace source is not SDK, and
  writes one line on the rule's user-facing log stream saying why.
- The rule's enabled flag and filters still apply to those traces.
- Documents the new meaning of the field in the API schema and the rules docs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation java Pull requests that update Java code Backend tests Including test files, or tests related like configuration. labels Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
🌐 typecheck — frontend Whole-project tsc type check 25.57s
☕ spotless — java backend Format Java code 7.18s
🌐 eslint — frontend Lint + autofix JS/TS 2.95s
Total (3 ran) 35.70s
⏭️ 40 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
⚓ helm-docs Regenerate Helm chart README ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🧪 rebaseline script tests Self-test the changelog re-baseline script ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️
🌈 zizmor — github workflows security Security-scan GitHub Actions workflows ⏭️

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🌿 Preview your docs: https://opik-preview-01a034bf-9f84-765f-bc1d-d695f61c38f6.docs.buildwithfern.com/docs/opik

No broken links found

Unverified links (timeout / rate-limited / server error — not failing the check)

https://aistudio.google.com/apikey (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.cloud.google.com/iam-admin/iam (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.cloud.google.com/iam-admin/roles (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.cloud.google.com/iam-admin/serviceaccounts (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.mistral.ai/api-keys/ (timeout)
↳ on page: /docs/opik/integrations/mistral
https://console.x.ai/ (403)
↳ on page: /docs/opik/integrations/xai-grok
https://docs.predibase.com/integrations/comet (403)
↳ on page: /docs/opik/integrations/predibase
https://opik-preview-01a034bf-9f84-765f-bc1d-d695f61c38f6.docs.buildwithfern.com/docs/opik/reference/typescript-sdk/evaluation/experiments (500)
↳ on page: /docs/opik/reference/overview
https://opik-preview-01a034bf-9f84-765f-bc1d-d695f61c38f6.docs.buildwithfern.com/docs/opik/v1/agent_optimization/quickstart_notebook (500)
↳ on page: /docs/opik/v1/agent_optimization/algorithms/gepa_optimizer
https://opik-preview-01a034bf-9f84-765f-bc1d-d695f61c38f6.docs.buildwithfern.com/docs/opik/v1/contributing/guides/local-development (500)
↳ on page: /docs/opik/v1/tracing/annotate_traces
https://opik-preview-01a034bf-9f84-765f-bc1d-d695f61c38f6.docs.buildwithfern.com/docs/opik/v1/integrations/mistral (500)
↳ on page: /docs/opik/v1/integrations/harbor
https://opik-preview-01a034bf-9f84-765f-bc1d-d695f61c38f6.docs.buildwithfern.com/docs/opik/v1/reference/rest-api/datasets/get-dataset-export-jobs (500)
↳ on page: /docs/opik/v1/reference/typescript-sdk/evaluation/datasets
https://opik-preview-01a034bf-9f84-765f-bc1d-d695f61c38f6.docs.buildwithfern.com/docs/opik/v1/reference/rest-api/workspaces/metrics-summary (500)
↳ on page: /docs/opik/v1/reference/typescript-sdk/evaluation/datasets
https://portal.azure.com/ (403)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://x.ai/ (403)
↳ on page: /docs/opik/integrations/xai-grok


📌 Results for commit b107407

jverre and others added 3 commits August 24, 2026 10:00
An out-of-range sampling rate passed @Valid and only failed on the
automation_rules.sampling_rate CHECK constraint, so the API answered 500
instead of 400. @DecimalMin/@DecimalMax on both the create and update DTOs
reject negative, greater-than-one, NaN and Infinity at the boundary.

The docs line also over-promised: a playground trace is scored only when it
carries selected_rule_ids, and optimization runs were left out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jverre
jverre marked this pull request as ready for review August 24, 2026 11:09
@jverre
jverre requested review from a team as code owners August 24, 2026 11:10
@CometActions

CometActions commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

This change looks worth a test.

OnlineScoringSampler now returns early for any non-SDK trace, so on a rule with trigger scope both an experiment/playground trace flips from "sampled at the configured rate" to "always scored" — a visible change in which traces carry the rule's feedback score. Our only sampling spec, online-evaluation-sampling-rate.spec.ts, seeds its whole 30-trace batch through the Python SDK, so every trace in it is still a logging source and the spec passes identically before and after; nothing exercises the new branch, and nothing covers the new @DecimalMin(0)/@DecimalMax(1) bound on samplingRate either. online-evaluation.sampling-rate is marked covered, but by that SDK-only test. Worth one deterministic test that puts both sources through a single rule.

Would target online-evaluation.sampling-rate.

What it would check
  1. Create a deterministic Python-Equals rule with trigger scope 'both' and sampling rate 0%, then seed matching traces via POST /v1/private/traces with source 'sdk' and source 'experiment'
  2. Confirm every experiment-source trace gets the rule's score and no SDK-source trace does — at rate 0 this is exact, no binomial band needed
  3. Repeat with source 'playground' on a default production-scope rule, since matchesTriggerScope lets playground/optimization through and they now bypass sampling too
  4. Check the rule's automation logs shows the new 'is not subject to the sampling rate ... applies to production traces only' line for the exempt traces
  5. POST and PATCH a rule with samplingRate -0.1 and 1.5 and confirm the new 0..1 bounds reject them, and that the dialog's percent field cannot produce one

Deploying a test environment for this PR and exploring it — results will follow in a comment.

also touches Backend (Java API / internal)
Run

Advisory, from the QA test radar. Nothing here blocks this PR, and anything it proposes is a draft for review.

Re-checked after a push on 24 Aug 17:12 UTC.

@CometActions CometActions added the test-environment Deploy Opik adhoc environment label Aug 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔄 Test environment deployment process has started

Phase 1: Deploying base version 2.2.37-6395 (from main branch) if environment doesn't exist
Phase 2: Building new images from PR branch jacques/OPIK-8040-sampling-production-only
Phase 3: Will deploy newly built version after build completes

You can monitor the progress here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@thiagohora thiagohora left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opik reviewer (mined from your team's review history)

7 findings — 0 high · 5 medium · 2 low. Suppressed by team conventions: see suppressed.md.

React 👍/👎 on each comment — your feedback helps tune what it flags.


private final float samplingRate;
@Schema(description = "Fraction of production (SDK-logged) traces the rule scores, from 0 to 1. Experiment, playground and optimization traces are always scored in full and ignore this value.")
@DecimalMin("0") @DecimalMax("1") private final float samplingRate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] New validation guard has no test on the update path

The new @DecimalMin("0")/@DecimalMax("1") guard is a semantically distinct validation branch on a public endpoint and nothing exercises it: no test asserts 422 for sampling_rate < 0 or > 1, nor 2xx for the inclusive boundaries 0 and 1. The sibling constraint on this very class (@SiZe(max = 150) on name, line 57) got dedicated 422 assertions for both create and update (AutomationRuleEvaluatorsResourceTest.java:959 and :979), so this change lacks the parity the team already established.

💡 Add a @ParameterizedTest over {-0.1, 1.1} asserting SC_UNPROCESSABLE_ENTITY (with the violation message) and over {0f, 1f} asserting success, for both POST /v1/private/automations/evaluators and PATCH/PUT of an existing rule.

rule: backend-testing-cover-every-new-branch-guard-and-edge-case-a-change-introduc

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 9fd8f74 addressed this comment by adding parameterized create and update tests for out-of-range sampling rates and inclusive 0/1 boundaries, including validation error and success assertions.


1. **Name:** The name of the rule
2. **Sampling rate:** The percentage of traces to score. When set to `100%`, all traces will be scored.
2. **Sampling rate:** The percentage of production traces to score. When set to `100%`, all production traces will be scored. This setting applies to production traces only — traces from experiments, the playground and optimization runs are never sampled.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] Playground/optimization traces are only scored when explicitly selected, not "never sampled"

For non-SDK, non-experiment sources (playground, optimization, evaluator) the trace never even reaches the sampling check unless it carries selected_rule_ids metadata: OnlineScoringSampler.sampleAndScore (apps/opik-backend/.../events/OnlineScoringSampler.java:220-241) adds such traces to scorableTraces only when extractSelectedRuleIds(trace) is non-empty, and skips the project batch otherwise. The sentence as written tells users every playground/optimization trace is scored regardless of the rate, when in reality most are not scored at all.

💡 State the actual behavior: playground and optimization traces are evaluated only when the user explicitly selects a rule for them, and in that case the sampling rate is ignored.

general finding

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 9fd8f74 addressed this comment by clarifying that playground and optimization traces ignore the sampling rate and are evaluated only when the rule is explicitly selected.

jverre and others added 3 commits August 24, 2026 18:01
The samplingRate @Schema sits on the sealed base class, so it reaches the
OpenAPI schema of all six subtypes. The previous wording described the
trace-level bypass, which is the opposite of what span and thread rules do:
OnlineScoringSpanSampler and TraceThreadOnlineScoringSamplerListener drop
non-SDK spans and threads before any rule is applied, so those are never
scored rather than scored in full.

Also states the decision the sampler owns instead of an outcome it cannot
guarantee: publishSampled is fire-and-forget, so a failed enqueue would
have left a user-facing "is scored" line for a trace that was never scored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both rules pages said non-production traces are always scored in full. That
is wrong twice over: thread and span rules never evaluate non-SDK data at
all, and playground or optimization traces are only evaluated when the user
explicitly selects the rule for that run.

Also brings the in-product tooltip in line with the docs and the API schema;
it still read "Percentage of traces to evaluate" and now follows the scope
switch the surrounding copy already uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n assertion

The [0,1] guard is a distinct validation branch on a public endpoint with no
coverage, while the sibling @SiZe(max = 150) constraint on the same class has
422 assertions for both create and update. Adds the same parity: parameterized
over {-0.1, 1.1} for 422 and {0, 1} for success, on create and update.

The python bypass test verified with any(), so it would have passed had the
bypass enqueued the wrong trace or built the message from the wrong evaluator.
Extracts toPythonMessage next to the existing toLlmMessage, reuses it in the
sibling test, and asserts the exact expected list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 10

 37 files   37 suites   7m 37s ⏱️
342 tests 339 ✅ 2 💤 0 ❌ 1 🔥
342 runs  337 ✅ 2 💤 0 ❌ 3 🔥

For more details on these errors, see this check.

Results for commit 9fd8f74.

@CometActions

Copy link
Copy Markdown
Collaborator

🌙 Nightly cleanup: The test environment for this PR (pr-7957) has been cleaned up to free cluster resources. PVCs are preserved — re-deploy to restore the environment.

@CometActions CometActions removed the test-environment Deploy Opik adhoc environment label Aug 25, 2026
@jverre
jverre merged commit 0bc5610 into main Aug 25, 2026
75 of 77 checks passed
@jverre
jverre deleted the jacques/OPIK-8040-sampling-production-only branch August 25, 2026 08:00
jverre added a commit that referenced this pull request Aug 25, 2026
Two files main added or changed under fern/docs/ collide with the removal
of that tree. Both changes already exist on their Opik 2 pages, so the v1
copies stay deleted:

- production/rules.mdx: the sampling-rate wording from #7957 is already in
  docs-v2/production/online-evaluation/rules.mdx, which merged cleanly.
- changelog/2026-08-24.mdx: #7968 added the same entry to docs-v2/changelog,
  and the existing /docs/opik/v1/:slug* redirect covers the old URL.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend baz: pending documentation Improvements or additions to documentation Frontend java Pull requests that update Java code 🟡 size/M tests Including test files, or tests related like configuration. typescript *.ts *.tsx

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants