Skip to content

[OPIK-8059] [BE][FE] fix: score playground dataset runs by the selected rules - #7989

Open
jverre wants to merge 7 commits into
mainfrom
jacques/OPIK-8059-playground-trigger-scope
Open

[OPIK-8059] [BE][FE] fix: score playground dataset runs by the selected rules#7989
jverre wants to merge 7 commits into
mainfrom
jacques/OPIK-8059-playground-trigger-scope

Conversation

@jverre

@jverre jverre commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Details

Before

  • A playground dataset run stamps source: experiment.
  • The sampler ignored selected_rule_ids on experiment traces and applied the trigger scope instead.
  • A rule scoped to production therefore never scored the run, and the metric tag spun forever.
  • The playground selected every rule by default, so the trigger scope had no effect there.

After

  • source = experiment:
    • Every rule in selected_rule_ids scores the trace, whatever its trigger scope, enabled flag, filters or sampling rate.
    • Every enabled rule scoped experiment or both also scores it, ignoring filters and sampling.
    • A rule qualifying both ways scores once.
  • source = sdk or null: enabled rules scoped production or both score the trace, with filters and sampling applied. selected_rule_ids is not read.
  • Any other source, playground included: not scored. A playground run without a dataset is a scratchpad, not production traffic.
  • Playground: a new experiment starts with no rule selected, and the output cell renders every score that arrives, not only the selected ones.

Change checklist

  • User facing
  • Documentation update

Issues

  • OPIK-8059

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 5
  • Scope: full implementation
  • Human verification: design decisions reviewed and approved by the author before implementation; automated checks run locally

Testing

  • mvn test -Dtest=OnlineScoringSamplerTest — 50 tests, 0 failures. Covers a selected rule that is disabled, filtered and sampled out; an unselected experiment-scoped rule scoring alongside the selected one; a rule qualifying on both paths enqueuing once; selected_rule_ids ignored on production traffic; and non-scorable sources skipped, playground among them.
  • mvn spotless:check — clean.
  • npm run lint and npm run typecheck — clean.
  • npx vitest run src/v2/pages/PlaygroundPage — 17 tests, 0 failures.
  • Manual pass on the PR test environment.
  • Not run: the backend integration suite and end-to-end tests.

Documentation

N/A

…ed rules

An experiment trace is now scored by the rules selected in the playground,
whatever their trigger scope, enabled flag, filters or sampling rate, and by
every enabled rule scoped to experiments. Playground traces without a dataset
are scored as production traffic.

The playground no longer selects every rule by default, and the output cell
renders every score that arrives instead of only the selected ones.

Implements OPIK-8059: Online evaluation rule with "Production traces" trigger
scope never scores a Playground dataset run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added java Pull requests that update Java code Frontend Backend tests Including test files, or tests related like configuration. typescript *.ts *.tsx labels Aug 25, 2026
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
🌐 typecheck — frontend Whole-project tsc type check 19.48s
🌐 eslint — frontend Lint + autofix JS/TS 4.27s
☕ spotless — java backend Format Java code 4.18s
🛡️ semgrep — java backend sql Block SQL injection-prone string formatting 1.77s
Total (4 ran) 29.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 ⏭️

Comment thread apps/opik-frontend/src/v2/pages/PlaygroundPage/MetricSelector.tsx
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Python SDK E2E Tests Results (Python 3.10)

296 tests  ±0   288 ✅ ±0   4m 21s ⏱️ -17s
  1 suites ±0     8 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 1a6560c. ± Comparison against base commit 14f554b.

♻️ This comment has been updated with latest results.

The refetch predicate stopped as soon as the selected rules had reported, so
scores from enabled rules targeting experiments arrived after the last fetch
and never reached the cell. Polling now waits for those rules too, while the
pending tags still come from the selected rules alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jverre jverre added the test-environment Deploy Opik adhoc environment label Aug 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔄 Test environment deployment process has started

Phase 1: Deploying base version 2.2.41-6430 (from main branch) if environment doesn't exist
Phase 2: Building new images from PR branch jacques/OPIK-8059-playground-trigger-scope
Phase 3: Will deploy newly built version after build completes

You can monitor the progress here.

Comment on lines +81 to +82
const awaitedScoreNamesRef = useRef<Set<string>>(new Set());
awaitedScoreNamesRef.current = new Set(scoreNamesOf(scoringRules));

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.

scoreNamesOf(scoringRules) reparses full code.metric for every rule on each render, and PlaygroundOutputTable mounts a PlaygroundOutputScoresContainer per cell, so trace polling repeatedly scans unbounded evaluator source — should we memoize the result with useMemo([scoringRules]) before assigning to awaitedScoreNamesRef.current and bound the source size or return score-name metadata from the server?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-frontend/src/v2/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutputScores/PlaygroundOutputScoresContainer.tsx`
around lines 81-82, the polling setup recomputes `scoreNamesOf(scoringRules)` on every
render, fully re-parsing each rule's `code.metric` and allocating a new `Set`. First,
wrap this derived awaited-score-name Set in `useMemo`, keyed by `scoringRules`, and
assign the memoized Set to `awaitedScoreNamesRef.current` so re-renders avoid redundant
scanning/parsing/allocation. Second, since `PlaygroundOutputTable` renders a
`PlaygroundOutputScoresContainer` per cell and the evaluator DTO only enforces
`@NotNull` (no size limit), large Python rules get repeatedly parsed by `scoreNamesOf`
across many cells during polling — enforce a strict maximum source length at the
evaluator create/update DTO or persistence boundary, and/or have the flow consume
bounded, server-provided score-name metadata instead of parsing unbounded `code.metric`
text in the render/polling path.

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 1a086df addressed this comment by memoizing the derived awaited score-name set with useMemo([scoringRules]) and storing that memoized set in the ref. The requested source-size bound or server-provided metadata was not added.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Correct, and fixed in 1a086df. The extraction now sits in a useMemo keyed on the rule set, so the Python source is parsed only when the rules change rather than on every render of every cell.

@CometActions

Copy link
Copy Markdown
Collaborator

Test environment is now available!

To configure additional Environment variables for your environment, run [Deploy Opik AdHoc Environment workflow] (https://github.com/comet-ml/comet-deployment/actions/workflows/deploy_opik_adhoc_env.yaml)

Access Information

The deployment has completed successfully and the version has been verified.

…e names

The rules list carries thread and span rules too. Their names could never
arrive in Trace.feedback_scores, so awaiting them kept the cell polling to the
300s ceiling. Restrict the awaited set to the trace-level rule types, which is
what the trace sampler publishes.

The name extraction also ran on every render, and it parses Python rule source
character by character, once per output cell. Memoize it on the rule set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jverre
jverre marked this pull request as ready for review August 25, 2026 19:26
@jverre
jverre requested a review from a team as a code owner August 25, 2026 19:26
@CometActions

CometActions commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Already covered by a test in this PR.

Both halves of this are already pinned by specs in QA draft #8000: playground/playground-selected-metrics.spec.ts asserts the selector opens at 0 of N, that a dataset run is scored by the ticked rule plus the experiment-scoped rule and by nothing else, and that the output rows carry exactly those chips; online-evaluation/online-evaluation-rule-filters.spec.ts asserts a rule's filter still gates its SDK trace but is ignored on its experiment trace. Those are proposed, not merged — you know better than we do whether they match what you intended, so they're worth a look.

Also already tested. online-evaluation/online-evaluation-sampling-rate.spec.ts landed on main after this branch's base, and its second test asserts the opposite of one line of this diff: it seeds playground- and optimization-source traces carrying selected_rule_ids and requires them scored, and it greps the log line the rate applies to production traces only. This PR drops both, so that test goes red on rebase. Nothing regresses for a user — only the playground writes selected_rule_ids, and a dataset run logs source=experiment — but the spec and the online-evaluation.sampling-rate note in taxonomy.yaml both document the old contract and need updating alongside this change.

Also worth a test. A ticked rule now bypasses isEnabled too, so a disabled rule a user picks in the selector scores the run. scoresPickedRuleThoughItIsDisabledFilteredAndSampledOut covers that at the sampler and says it is deliberate; no e2e does — draft #8000 seeds only enabled rules, and online-evaluation-enable-disable-rule.spec.ts only proves a disabled rule stops scoring SDK traces. One extra seeded rule in the open draft closes it; it does not need a spec of its own.

areas: playground · 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 26 Aug 14:36 UTC — nothing the verdict depends on changed.

@CometActions

Copy link
Copy Markdown
Collaborator

🌙 Nightly cleanup: The test environment for this PR (pr-7989) 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 26, 2026
@miguelgrc miguelgrc added the test-environment Deploy Opik adhoc environment label Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔄 Test environment deployment process has started

Phase 1: Deploying base version 2.2.41-6430 (from main branch) if environment doesn't exist
Phase 2: Building new images from PR branch jacques/OPIK-8059-playground-trigger-scope
Phase 3: Will deploy newly built version after build completes

You can monitor the progress here.

@miguelgrc miguelgrc 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)

6 findings — 1 high · 4 medium · 1 low. Suppressed by team conventions: see suppressed.md.

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

Comment thread apps/opik-frontend/src/v2/pages/PlaygroundPage/metricSelection.ts Outdated
@CometActions

Copy link
Copy Markdown
Collaborator

Test environment is now available!

To configure additional Environment variables for your environment, run [Deploy Opik AdHoc Environment workflow] (https://github.com/comet-ml/comet-deployment/actions/workflows/deploy_opik_adhoc_env.yaml)

Access Information

The deployment has completed successfully and the version has been verified.

A playground run with no dataset is a scratchpad, not production traffic, so
production-scoped rules should not judge it. Only experiment traces and SDK
traces are scored now.

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

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 16

 30 files  + 3   30 suites  +3   3m 18s ⏱️ +19s
302 tests + 9  296 ✅ + 6  5 💤 +2  0 ❌ ±0  1 🔥 +1 
210 runs   - 83  204 ✅  - 86  5 💤 +2  0 ❌ ±0  1 🔥 +1 

For more details on these errors, see this check.

Results for commit 336f1df. ± Comparison against base commit 14f554b.

This pull request removes 36 and adds 45 tests. Note that renamed tests count towards both.
com.comet.opik.api.resources.v1.events.OnlineScoringSpanSamplerIntegrationTest ‑ redisProducerAndConsumerBaseFlowForSpans(Source, OnlineScoringSpanSampler)[1]
com.comet.opik.api.resources.v1.events.OnlineScoringSpanSamplerIntegrationTest ‑ redisProducerAndConsumerBaseFlowForSpans(Source, OnlineScoringSpanSampler)[2]
com.comet.opik.api.resources.v1.priv.AgentInsightsJobsResourceTest ‑ create__firstThenConflict
com.comet.opik.api.resources.v1.priv.AgentInsightsJobsResourceTest ‑ create__projectMissing__returns404
com.comet.opik.api.resources.v1.priv.AgentInsightsJobsResourceTest ‑ cronSweep__triggersJobsWithTraces
com.comet.opik.api.resources.v1.priv.AgentInsightsJobsResourceTest ‑ get__afterCreateAndWhenAbsent
com.comet.opik.api.resources.v1.priv.AgentInsightsJobsResourceTest ‑ reportFailure__createAndRead
com.comet.opik.api.resources.v1.priv.AgentInsightsJobsResourceTest ‑ reportFailure__invalidBody__returns422(String, ReportFailure)[1]
com.comet.opik.api.resources.v1.priv.AgentInsightsJobsResourceTest ‑ reportFailure__invalidBody__returns422(String, ReportFailure)[2]
com.comet.opik.api.resources.v1.priv.AgentInsightsJobsResourceTest ‑ reportFailure__invalidBody__returns422(String, ReportFailure)[3]
…
com.comet.opik.api.resources.v1.events.DatasetExportJobSubscriberResourceTest$ConfigurationTests ‑ shouldVerifyStreamConfiguration
com.comet.opik.api.resources.v1.events.DatasetExportJobSubscriberResourceTest$ConfigurationTests ‑ shouldVerifySubscriberIsEnabled
com.comet.opik.api.resources.v1.events.DatasetExportJobSubscriberResourceTest$EdgeCaseTests ‑ shouldCompleteExport_whenDatasetDoesNotExist
com.comet.opik.api.resources.v1.events.DatasetExportJobSubscriberResourceTest$SuccessTests ‑ shouldProcessExportJobSuccessfully_forEmptyDataset
com.comet.opik.api.resources.v1.events.DatasetExportJobSubscriberResourceTest$SuccessTests ‑ shouldProcessExportJobSuccessfully_whenDatasetHasItems
com.comet.opik.api.resources.v1.events.DatasetExportJobSubscriberResourceTest$SuccessTests ‑ shouldProcessExportJobWithLargeDataset
com.comet.opik.api.resources.v1.events.DatasetExportJobSubscriberResourceTest$SuccessTests ‑ shouldProcessMultipleExportJobsInParallel
com.comet.opik.api.resources.v1.priv.DatasetsResourceProjectScopedTest ‑ createDatasetWithExistingProjectName
com.comet.opik.api.resources.v1.priv.DatasetsResourceProjectScopedTest ‑ createDatasetWithNonExistingProjectId
com.comet.opik.api.resources.v1.priv.DatasetsResourceProjectScopedTest ‑ createDatasetWithNonExistingProjectName
…
This pull request removes 1 skipped test and adds 3 skipped tests. Note that renamed tests count towards both.
com.comet.opik.infrastructure.aws.rds.MysqlRdsIamE2eTest ‑ testAwsRds__whenRdsIamDbAuthenticationIsEnabled__shouldAcceptRequest
com.comet.opik.api.resources.v1.events.DatasetExportJobSubscriberResourceTest$SuccessTests ‑ shouldProcessExportJobSuccessfully_whenDatasetHasItems
com.comet.opik.api.resources.v1.events.DatasetExportJobSubscriberResourceTest$SuccessTests ‑ shouldProcessExportJobWithLargeDataset
com.comet.opik.api.resources.v1.events.DatasetExportJobSubscriberResourceTest$SuccessTests ‑ shouldProcessMultipleExportJobsInParallel

♻️ This comment has been updated with latest results.

Comment on lines +233 to 235
} else if (Source.isLoggingSource(trace.source())) {
scorableTraces.add(trace);
}

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.

Source.isLoggingSource accepts only SDK and null, so standalone Source.PLAYGROUND traces are filtered before reaching ruleEvaluatorService.findAll or message publication — should we confirm whether they should score? If filtering is intentional, should we switch OnlineScoringEngineTest.testFilteringEvaluatorsByTraceMetadata to Source.EXPERIMENT and add a real TraceService/EventBus assertion that playground traces with selected_rule_ids are not scored; otherwise include Source.PLAYGROUND while preserving the production trigger checks?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code and clarify intended
behavior for `Source.PLAYGROUND` traces in
`apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/events/OnlineScoringSampler.java`
around lines 233-235. Option A (if playground traces should be scorable): update the
`sampleAndScore` trace eligibility logic so standalone `Source.PLAYGROUND` traces are
included when no dataset is selected, extending the fallback condition or helper
(currently based on `Source.isLoggingSource`) to accept PLAYGROUND, while preserving the
existing production trigger checks and selected-rule handling. Option B (if the
PLAYGROUND filtering is intentional): keep the `Source.isLoggingSource` gate as-is, but
update `OnlineScoringEngineTest.testFilteringEvaluatorsByTraceMetadata` to use
`Source.EXPERIMENT` instead of `Source.PLAYGROUND` so it continues validating scoring
with selected rule IDs, and add a separate test asserting that a playground trace with
selected IDs produces no scoring — preferably exercising the real
`TraceService`/`EventBus` flow rather than manually constructing the event and mocking
the score service.

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 336f1df addressed this comment by explicitly limiting selected-rule scoring to Source.EXPERIMENT and excluding standalone playground traces, while preserving logging-source scoring. The requested dedicated regression test is not shown.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Filtering is intentional and was decided explicitly: a playground run without a dataset is a scratchpad, not production traffic, so it is not scored at all. The ticket description records the contract. OnlineScoringEngineTest.testFilteringEvaluatorsByTraceMetadata is switched to Source.EXPERIMENT in 2f82d81, as you suggest. The "playground traces are not scored" assertion already exists in OnlineScoringSamplerTest.skipsTracesFromNonScorableSources, which is parameterized over every source except SDK and EXPERIMENT — so PLAYGROUND, OPTIMIZATION and EVALUATOR are all covered.

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.

Thanks for clarifying the intended scratchpad behavior and existing coverage. I’ll save this context to memory once the PR is merged.

jverre and others added 2 commits August 26, 2026 15:33
With nothing selected and no experiment-scoped rules, every output cell polled
its trace every 5s for the full 5 minutes with nothing to wait for. Gate the
query on there being a rule that will score the trace, which is distinct from
being able to predict its score names.

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

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 13

 33 files   - 22   33 suites   - 22   5m 37s ⏱️ +17s
283 tests  - 36  280 ✅  - 37  2 💤 ±0  0 ❌ ±0  1 🔥 +1 
279 runs   - 35  276 ✅  - 36  2 💤 ±0  0 ❌ ±0  1 🔥 +1 

For more details on these errors, see this check.

Results for commit 336f1df. ± Comparison against base commit 14f554b.

This pull request removes 121 and adds 85 tests. Note that renamed tests count towards both.
com.comet.opik.api.resources.v1.jobs.TraceThreadsClosingJobTest$TraceThreadsClosingJob ‑ shouldCloseTraceThreadsForProject
com.comet.opik.api.resources.v1.jobs.TraceThreadsClosingJobTest$TraceThreadsClosingJob ‑ shouldCloseTraceThreadsForProjectWithCustomTimeout
com.comet.opik.api.resources.v1.jobs.TraceThreadsClosingJobTest$TraceThreadsClosingJob ‑ shouldCloseTraceThreadsWhenWorkspaceConfigurationTimeoutIsNull
com.comet.opik.api.resources.v1.jobs.TraceThreadsClosingJobTest$TraceThreadsClosingJob ‑ shouldReopenTraceThreadsIfNewTracesAreAdded
com.comet.opik.api.resources.v1.priv.DatasetExperimentE2ETest$FilterDatasetsByExperimentWith ‑ when__filteringByDatasetsWithExperimentsAfterAnExperimentIsDeleted__thenShouldReturnTheDatasetWithExperiments
com.comet.opik.api.resources.v1.priv.DatasetExperimentE2ETest$FilterDatasetsByExperimentWith ‑ when__filteringByDatasetsWithExperimentsAfterDeletingExperimentsButDatasetHasMore__thenShouldReturnTheDatasetWithExperiments
com.comet.opik.api.resources.v1.priv.DatasetExperimentE2ETest$FilterDatasetsByExperimentWith ‑ when__filteringByDatasetsWithExperiments__thenShouldReturnTheDatasetWithExperiments
com.comet.opik.api.resources.v1.priv.ExperimentsResourceCustomConfigurationTest ‑ findExperimentsWithForceSortingBypassesLimit
com.comet.opik.api.resources.v1.priv.ExperimentsResourceCustomConfigurationTest ‑ findExperimentsWithSortingDisabled
com.comet.opik.api.resources.v1.priv.LocalRunnersResourceTest ‑ connectHappyPath
…
com.comet.opik.api.resources.v1.events.webhooks.AlertBucketServiceTest ‑ addEventToBucket__whenAddingToSameAlertAfterConfigChange__shouldUseOriginalWindow
com.comet.opik.api.resources.v1.events.webhooks.AlertBucketServiceTest ‑ addEventToBucket__whenConfigChanges__shouldCreateSeparateBucketsWithDifferentWindows
com.comet.opik.api.resources.v1.events.webhooks.AlertBucketServiceTest ‑ addEventToBucket__whenFirstEvent__shouldSetTtl
com.comet.opik.api.resources.v1.events.webhooks.AlertBucketServiceTest ‑ addEventToBucket__whenFirstEvent__shouldStoreWindowSizeFirstSeenAndWorkspaceId
com.comet.opik.api.resources.v1.events.webhooks.AlertBucketServiceTest ‑ addEventToBucket__whenSubsequentEvents__shouldNotRefreshTtl
com.comet.opik.api.resources.v1.events.webhooks.AlertBucketServiceTest ‑ addEventToBucket__whenSubsequentEvents__shouldPreserveOriginalWindowSize
com.comet.opik.api.resources.v1.events.webhooks.AlertBucketServiceTest ‑ deleteBucket__shouldRemoveBucketFromRedis
com.comet.opik.api.resources.v1.events.webhooks.AlertBucketServiceTest ‑ getBucketData__shouldReturnAllEventIdsAndPayloads
com.comet.opik.api.resources.v1.events.webhooks.AlertBucketServiceTest ‑ getBucketsReadyToProcess__afterConfigChange__shouldUseStoredWindowSizes
com.comet.opik.api.resources.v1.priv.DatasetsResourceCreateFromTracesTest ‑ createDatasetItemsFromTraces__success
…

Comment on lines 47 to 50
const { data: rulesData, isSuccess: rulesLoaded } = useRulesList(
{
workspaceName,
projectId: activeProjectId ?? undefined,

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.

Rules beyond first 100 never refresh scores

The component fetches only page 1 with size: 100 even though useRulesList returns total, so an enabled experiment/both rule beyond the first 100 is missing from scoringRules, leaving hasScoringRules false and disabling useTraceById; should we paginate all rules or use the existing all-rules loading pattern so late feedback scores render?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-frontend/src/v2/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutputScores/PlaygroundOutputScoresContainer.tsx`
around lines 47-53, update the `PlaygroundOutputScoresContainer` rule-loading logic so
it does not inspect only page 1 with `size: 100`. Fetch all rule pages using the
pagination metadata, or adopt the existing sufficiently large/all-rules loading pattern,
before computing `scoringRules` and `hasScoringRules`, ensuring experiment/both rules
beyond the first page keep `useTraceById` enabled.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed in 2f82d81, though not by paginating. The gate now also checks total > rules.length: when the list is truncated we cannot know whether a scoring rule sits beyond page 1, so polling stays on. That keeps late scores rendering without a second request per cell. Fetching every page is the fuller fix and belongs with the select-all pagination gap tracked separately.

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 2f82d81 addressed this comment by keeping polling enabled when the rules response is truncated, so scoring rules beyond the first page no longer disable useTraceById.

Comment on lines +75 to +83
const scoringRules = useMemo(
() =>
rules.filter(
(rule) =>
TRACE_RULE_TYPES.includes(rule.type) &&
(selectedRuleIdsSet.has(rule.id) ||
(rule.enabled !== false &&
(rule.trigger_scope === EVAL_TRIGGER_SCOPE.experiment ||
rule.trigger_scope === EVAL_TRIGGER_SCOPE.both))),

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.

Disabled Python rules cause endless polling

scoringRules includes user_defined_metric_python rules even when ServiceTogglesConfig.pythonEvaluatorEnabled is false, so the backend produces no score for them while the frontend polls indefinitely — should we apply the same toggle when building scoringRules, or carry the backend capability into this decision?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-frontend/src/v2/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutputScores/PlaygroundOutputScoresContainer.tsx`
around lines 75-83, update the `scoringRules` derivation so `user_defined_metric_python`
rules are included only when the backend Python evaluator capability is enabled. Obtain
this capability from the existing frontend configuration or API state, and ensure
Python-only rules do not enable polling when the backend cannot publish their scores; if
appropriate, expose the disabled capability to the user instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not fixable from here: pythonEvaluatorEnabled is a server-side toggle the frontend cannot observe, and there is no endpoint exposing it. It is also unchanged from main, where a selected Python rule with the toggle off polled to the same ceiling. Leaving as is; the cost is polling duration, not a wrong result.

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.

Thanks for the clarification—I'll remember this constraint after the PR is merged.

Comment on lines +88 to +91
// With no rule to score this trace there is nothing to poll for. Until the rules arrive we
// cannot tell, so the query stays enabled while the list is still loading.
const rulesPending = !!activeProjectId && !rulesLoaded;
const hasScoringRules = rulesPending || scoringRules.length > 0;

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.

Rules errors cause prolonged trace polling

rulesPending relies on !isSuccess, so a terminal useRulesList error is still treated as pending, keeping hasScoringRules true and useTraceById polling every 5 seconds for up to 300,000 ms while the failure is silently ignored — should we check isError to stop polling and surface the error instead?

Severity web_search

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-frontend/src/v2/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutputScores/PlaygroundOutputScoresContainer.tsx`
around lines 88-91, fix the `rulesPending`/`hasScoringRules` logic so a terminal
`useRulesList` error is not treated as an unknown pending rules set that enables
`useTraceById` polling. Read the query's error state, disable trace fetching when rule
discovery fails, and surface the rules-loading error through the component's UI or error
state instead of silently continuing requests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Correct, and fixed in 2f82d81. rulesPending now excludes the error state, so a failed rules lookup stops polling rather than counting as still-loading. Surfacing the error in the cell is a separate concern — there is no UI slot for it today.

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.

Thanks, that addresses the polling issue. I’ll save this context to memory once the PR is merged.

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 2f82d81 addressed this comment by excluding failed rule queries from rulesPending, preventing trace polling from continuing to the timeout. It does not appear to surface the rules error in the UI.

Comment on lines 106 to 110
const { data: trace } = useTraceById(
{ traceId: traceId! },
{
enabled: !!traceId && hasRulesSelected,
enabled: !!traceId && hasScoringRules,
refetchInterval: (query) => {

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.

Large runs create unbounded trace polling fan-out

The broadened enabled gate runs useTraceById for every mounted score cell, so a 1,000-row page with N prompt columns creates 1,000 × N polling loops and a request load without a fixed budget — should we virtualize the score cells or impose a concurrency/request budget before enabling per-cell polling?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-frontend/src/v2/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutputScores/PlaygroundOutputScoresContainer.tsx`
around lines 106-110, the `PlaygroundOutputScoresContainer` enables an independently
polling `useTraceById` query for every mounted score cell, causing request volume to
grow with the number of rows and prompt columns. Refactor the output rendering/query
coordination to virtualize score cells or enforce a fixed concurrency and request budget
before enabling per-cell polling, so a 1,000-row page and many prompts cannot create
unbounded polling traffic. Preserve the existing score completion and timeout behavior
within that bound.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

One container per cell is the existing shape, not something this PR introduces, and the gate narrows it rather than widening it. On main every cell polled unconditionally for the full 300s, including when the project had no rules at all. Here a cell polls only when a rule can actually score the trace. Virtualizing the score cells is a table concern and out of scope for this change.

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.

Thanks for the context—this gate narrows the existing polling behavior, and score-cell virtualization is outside this change’s scope. I’ll save this to memory once the PR is merged.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 7

65 tests   - 1 596   55 ✅  - 1 606   10m 24s ⏱️ +12s
16 suites  -    11    0 💤 ±    0 
16 files    -    11    9 ❌ +    9   1 🔥 +1 

For more details on these failures and errors, see this check.

Results for commit 336f1df. ± Comparison against base commit 14f554b.

This pull request removes 1623 and adds 27 tests. Note that renamed tests count towards both.
com.comet.opik.api.resources.v1.events.TraceThreadOnlineScoringSamplerListenerIntegrationTest ‑ processesThreadsWithSdkOrNullSource(Source, TraceThreadOnlineScoringSamplerListener)[1]
com.comet.opik.api.resources.v1.events.TraceThreadOnlineScoringSamplerListenerIntegrationTest ‑ processesThreadsWithSdkOrNullSource(Source, TraceThreadOnlineScoringSamplerListener)[2]
com.comet.opik.api.resources.v1.events.TraceThreadOnlineScoringSamplerListenerIntegrationTest ‑ skipsNonSdkThreads(Source, TraceThreadOnlineScoringSamplerListener)[1]
com.comet.opik.api.resources.v1.events.TraceThreadOnlineScoringSamplerListenerIntegrationTest ‑ skipsNonSdkThreads(Source, TraceThreadOnlineScoringSamplerListener)[2]
com.comet.opik.api.resources.v1.events.TraceThreadOnlineScoringSamplerListenerIntegrationTest ‑ skipsNonSdkThreads(Source, TraceThreadOnlineScoringSamplerListener)[3]
com.comet.opik.api.resources.v1.events.TraceThreadOnlineScoringSamplerListenerIntegrationTest ‑ skipsNonSdkThreads(Source, TraceThreadOnlineScoringSamplerListener)[4]
com.comet.opik.api.resources.v1.priv.AuthenticationResourceTest$ApiKey ‑ checkAccessForDefaultWorkspace__whenApiKeyIsPresent__thenReturnProperResponse(String, int, String)[1]
com.comet.opik.api.resources.v1.priv.AuthenticationResourceTest$ApiKey ‑ checkAccessForDefaultWorkspace__whenApiKeyIsPresent__thenReturnProperResponse(String, int, String)[2]
com.comet.opik.api.resources.v1.priv.AuthenticationResourceTest$ApiKey ‑ checkAccessForDefaultWorkspace__whenApiKeyIsPresent__thenReturnProperResponse(String, int, String)[3]
com.comet.opik.api.resources.v1.priv.AuthenticationResourceTest$ApiKey ‑ checkAccess__whenApiKeyIsPresent__thenReturnProperResponse(String, int, String)[1]
…
com.comet.opik.api.resources.v1.priv.AttachmentResourceTest ‑ directS3DownloadShouldFailTest
com.comet.opik.api.resources.v1.priv.AttachmentResourceTest ‑ directS3UploadShouldFailTest
com.comet.opik.api.resources.v1.priv.AttachmentResourceTest ‑ uploadAttachmentWithMultiPartPresignUrl
com.comet.opik.api.resources.v1.priv.DashboardsResourceProjectScopedTest ‑ createDashboardWithExistingProjectName
com.comet.opik.api.resources.v1.priv.DashboardsResourceProjectScopedTest ‑ createDashboardWithNonExistingProjectId
com.comet.opik.api.resources.v1.priv.DashboardsResourceProjectScopedTest ‑ createDashboardWithNonExistingProjectName
com.comet.opik.api.resources.v1.priv.DashboardsResourceProjectScopedTest ‑ createDashboardWithProjectId
com.comet.opik.api.resources.v1.priv.DashboardsResourceProjectScopedTest ‑ findDashboardsByProjectId
com.comet.opik.api.resources.v1.priv.OllamaResourceTest ‑ listModels__emptyList(ClientSupport)
com.comet.opik.api.resources.v1.priv.OllamaResourceTest ‑ listModels__success(ClientSupport)
…

OnlineScoringEngineTest still encoded the old playground contract: it built a
playground trace naming two of three rules and asserted six scores. Playground
traces are no longer scored, so it now uses an experiment trace, where the two
picked rules score and the third, scoped to production, stays out.

The polling gate treated a failed rules lookup as still loading, which kept the
cell polling to the timeout, and it could not tell that the rules list is capped
at one page. Stop on a failed lookup, keep polling when the list is truncated.

Also corrects a comment that described null selection as a legacy value when the
store still writes it, and adds sampler coverage for a picked Python rule with
the evaluator toggle on and off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment on lines +1 to +3
// Metric selection is a plain list of rule ids. Both an empty list and null mean nothing is
// selected -- the store still writes null whenever a dataset has no stored selection -- and the
// run is then scored only by the rules that target experiments.

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.

Persisted selections silently lose all rules

Persisted PLAYGROUND_STATE entries with scoresByDatasetId[datasetId] = null now mean “none selected” instead of the legacy “all metrics selected,” so RunExperimentControl reads them with ?? null and createLogPlaygroundProcessor omits selected_rule_ids, letting upgraded users run existing datasets without the rules previously selected by default. Should we add an explicit migration/versioned fallback for legacy null, or document and intentionally gate this clean break?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-frontend/src/v2/pages/PlaygroundPage/metricSelection.ts` around lines 1-3,
preserve the legacy meaning of persisted `scoresByDatasetId[datasetId] = null` as “all
metrics selected” instead of redefining it as “none selected” for existing
`PLAYGROUND_STATE` entries. Add an explicit persisted-state migration/version marker or
a legacy fallback before `RunExperimentControl` and `createLogPlaygroundProcessor`
consume the selection, while retaining `[]` for an intentional empty selection. Update
the relevant tests to cover upgraded datasets with legacy `null` values.

Comment on lines +47 to +51
const {
data: rulesData,
isSuccess: rulesLoaded,
isError: rulesFailed,
} = useRulesList(

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.

Score polling state machine lacks coverage

useRulesList/useTraceById lack a focused component/query-seam test, so their loading/error, traceId/activeProjectId null, pagination/name-resolution, and rule-scope branches that control polling and rendered metric sets remain unverified. Should we add a mocked-hook container/query test with explicit assertions for each scenario, as required by .agents/skills/opik-frontend/testing.md and AGENTS.md?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-frontend/src/v2/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutputScores/PlaygroundOutputScoresContainer.tsx`
around lines 47-51, add a focused test for the `useRulesList`/`useTraceById`
state-machine seam used by `PlaygroundOutputScoresContainer`. Mock both hooks and assert
`enabled`, polling behavior, and rendered metric names for loading, terminal-error, and
null `traceId`/`activeProjectId` cases, including `total > content.length` and awaited
names already present. Also cover selected, implicit experiment-scoped, thread/span, and
Python rule combinations, following the frontend testing guidance and repository test
organization rules.

Comment on lines +95 to +98
const rulesPending = !!activeProjectId && !rulesLoaded && !rulesFailed;
const rulesTruncated = (rulesData?.total ?? 0) > rules.length;
const hasScoringRules =
rulesPending || rulesTruncated || scoringRules.length > 0;

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.

Polling stops after a failed useRulesList lookup or when page-1 awaitedScoreNames is present despite rulesTruncated, while the backend's unpaginated findAll(projectId, workspaceId) can still evaluate later-page rules, so their scores are never fetched or rendered — should we keep polling through MAX_REFETCH_TIME for unknown rule state and while rulesTruncated is true, or load all pages first?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-frontend/src/v2/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutputScores/PlaygroundOutputScoresContainer.tsx`
around lines 95-98, fix the `rulesPending`/`hasScoringRules` gating logic used to
control `refetchInterval` for the trace query so it doesn't stop polling prematurely in
two scenarios: (1) a failed `useRulesList` request should be treated as an unknown rule
state rather than a confirmed absence of scoring rules, keeping the trace query enabled
and polling through the existing `MAX_REFETCH_TIME` timeout; (2) polling should not stop
once page-1 `awaitedScoreNames` is present if `rulesTruncated` is true (i.e.,
`rulesData.total > rules.length`), since the backend's unpaginated `findAll(projectId,
workspaceId)` may still evaluate rules beyond page 1. Only stop polling once a
successful, complete rule lookup (all pages loaded) confirms there are no scoring rules,
or explicitly keep polling while `rulesTruncated` is true.

Comment on lines +453 to 455
var trace = createTrace(traceId, projectId, Source.EXPERIMENT).toBuilder()
.metadata(metadata)
.build();

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.

Cross-layer scoring contract remains untested

The new engine test constructs Trace directly with Source.EXPERIMENT and selected_rule_ids, bypassing buildLogProcessor/snakeCaseObj and POST /v1/private/traces/batch, so serialization or selection regressions can leave tests green while changing which rules are scored — should we add a producer-to-ingestion contract test covering experiment runs with [] and selected IDs, scratchpad playground runs with null/[], and applicable malformed or legacy-null inputs, as AGENTS.md requires?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/events/OnlineScoringEngineTest.java`
around lines 453-455, update `testFilteringEvaluatorsByTraceMetadata` so it does not
rely solely on a directly constructed `Trace` with `Source.EXPERIMENT` and
`selected_rule_ids`. Add a focused producer-to-ingestion or public `POST
/v1/private/traces/batch` integration test that exercises frontend serialization and
deserialization for experiment traces with empty and selected rule IDs,
scratchpad/playground traces with null and empty selections, and applicable malformed or
legacy-null inputs. Assert the resulting scoring behavior and source/selection fields so
regressions across the boundary cannot leave the engine tests passing.

Comment on lines +280 to +291
void scoresPickedPythonRuleOnExperimentTracesWhenToggleIsEnabled() {
when(serviceTogglesConfig.isPythonEvaluatorEnabled()).thenReturn(true);
var evaluator = createPythonEvaluator(0.0f, EvalTriggerScope.PRODUCTION);
var trace = createTrace(Source.EXPERIMENT).toBuilder()
.metadata(metadataWithRuleIds(evaluator.getId()))
.build();
whenFindAllPythonEvaluators(evaluator);

onlineScoringSampler.onTracesCreated(new TracesCreated(List.of(trace), workspaceId, userName));

verify(onlineScorePublisher).enqueueMessage(List.of(toPythonMessage(evaluator, trace)),
AutomationRuleEvaluatorType.USER_DEFINED_METRIC_PYTHON);

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.

Duplicated cases drift over time

The enabled and disabled toggle cases duplicate the evaluator, trace, stubbing, invocation, and message setup, so equivalent permutations can drift in coverage when fixtures change — should we consolidate them into a @ParameterizedTest/@MethodSource supplying the toggle value and VerificationMode (times(1) versus never()), while sharing the exact message verification?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/events/OnlineScoringSamplerTest.java`
around lines 280-297, refactor
`scoresPickedPythonRuleOnExperimentTracesWhenToggleIsEnabled` and
`skipsPickedPythonRuleOnExperimentTracesWhenToggleIsDisabled` into one
`@ParameterizedTest` backed by a `@MethodSource`. Supply the Python evaluator toggle
value and the appropriate Mockito `VerificationMode` (`times(1)` or `never()`), keeping
the shared evaluator, trace, stubbing, invocation, and message-verification setup
centralized so both cases retain equivalent coverage.

@CometActions

Copy link
Copy Markdown
Collaborator

🌙 Nightly cleanup: The test environment for this PR (pr-7989) 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 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend baz: pending Frontend java Pull requests that update Java code 🟠 size/L 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