Sitelet https://github.com/comet-ml/opik/pull/7970
Skip to content

[OPIK-8021] [QA] Proposed e2e specs for the virtualized traces table (from #7947 exploration) - #7970

Draft
CometActions wants to merge 6 commits into
mainfrom
comet-qa-bot/OPIK-8021/traces-table-virtualization-e2e
Draft

[OPIK-8021] [QA] Proposed e2e specs for the virtualized traces table (from #7947 exploration)#7970
CometActions wants to merge 6 commits into
mainfrom
comet-qa-bot/OPIK-8021/traces-table-virtualization-e2e

Conversation

@CometActions

@CometActions CometActions commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Generated by the release QA side flow — draft, needs human review before merge.
Nothing here has been reviewed by a person. Two specs were written from an
exploratory testing pass and run green against the source PR's own deployed
environment; a third candidate was dropped (see below).

Where these came from

Exploratory testing of #7947 — "OPIK-8021 [FE] perf: virtualize
the traces table's columns and rows" — against that PR's own deployed
environment (https://pr-7947.dev.comet.com, serving 2.2.37-7947-merge-3045,
OSS install, workspace default). A human worked the flows by hand there first;
these specs automate the two that held up.

This PR targets andriid/OPIK-8021-traces-table-columns-perf, not main.
The behaviour asserted here — a traces table that renders a moving window of
rows and columns instead of all of them — only exists on that branch. On main
the table still renders every row, so the specs' own precondition checks
(fewer rows in the DOM than were seeded, spacer cells present in the header
row
) would fail. Rebase this onto main once #7947 lands.

Written and verified against 4b66cdd4c1d0583a08ffd574fb983371c40bba8b, which
was #7947's head at the time.

The specs

Both live in tests_end_to_end/e2e/tests/trace-explore/traces-table-virtualization.spec.ts.

1. @cap:traces.list-traces — row window renders every trace exactly once

Seeds 60 traces, reads the API's ordering for the same project, then sweeps the
page body from top to bottom in 250px steps. At every offset it asserts the
rendered data-row-ids are a contiguous slice of the API's ordering starting
where they claim to, and that no id is in the DOM twice at once. Across the whole
sweep, every seeded trace must have been seen, and the final window must be the
tail of the ordering.

The failure this is for is a row silently dropped or repeated at a window seam —
which renders as a plausible-looking table, not as an error. traces.list-traces
is nominally covered by trace-explore-smoke.spec.ts, but that spec seeds three
traces and so never crosses the 25-row threshold; this is the only spec that
reaches the windowed code path at all.

Verification: passed. npx playwright test tests/trace-explore/traces-table-virtualization.spec.ts
against https://pr-7947.dev.comet.com, green on 4 consecutive runs (~24s each).

2. @cap:traces.configure-columns — column window keeps everything aligned

Gives the same 60 traces 45 distinct feedback-score names, which the Logs table
offers as 45 extra columns (67 in total), then turns every column on from the
Columns picker. At five horizontal scroll offsets it reads the header's column
sequence — including the aria-hidden spacer cells, which are positions in the
row like any other — and asserts every rendered row's cell sequence and the
<colgroup> match it position for position
, and that the sampled sequences
are not all identical (i.e. the window actually moved). Finally, at maximum
scrollLeft, the pinned select column must be position: sticky at left: 0
and geometrically flush with the scroll container's left edge, on the header and
on every rendered row.

The failure this is for is a spacer of the wrong width shifting every cell one
column across: correct data under the wrong header, which looks like correct
data. traces.configure-columns had no spec at all before this.

Verification: passed. Same command and environment, green on 4 consecutive
runs (~16s each).

What this change also touches

  • fixtures/windowed-traces.fixture.ts (new) — windowedTraces seeds 60
    traces in one batch write with caller-minted UUIDv7 ids one second apart, then
    polls until the project lists exactly those ids and returns them in the API's
    order. wideTracesTable chains from it and adds the 45 score names. Sixty
    rather than the forty the exploration used, so "fewer rows in the DOM than were
    seeded" has ~20 rows of headroom rather than 2 and does not depend on the
    runner's viewport height. Teardown deletes the traces explicitly — deleting a
    project does not cascade to them, and global-teardown's prefix sweep only
    knows about experiments, datasets and projects.
  • pom/logs.page.ts — window-reading helpers (header/cell/colgroup
    sequences, scroll extent, a scroll-and-settle step, pinned-column geometry) and
    the Columns picker. No raw selectors leak into the spec.
  • core/backend/client.tscreateTracesBatch and scoreTracesBatch. The
    Python bridge creates one trace per call and flushes each time, which is right
    for a three-trace fixture and not for sixty traces carrying 2,700 scores.
  • coverage/taxonomy.yaml — spec added to the traces area's specs: list;
    configure-columns flipped to covered: true, tier: t2-cuj. list-traces was
    already covered: true at t1-smoke and is left as-is.

Two things a reviewer should look at

  1. The scroll container has no data-testid. PageBodyScrollContainer.tsx
    renders the element the table virtualizes against, and its only distinguishing
    marks are Tailwind classes. The POM resolves it as the nearest scrollable
    ancestor of [data-table-wrapper]
    — semantic rather than class-coupled, and
    the same lookup the virtualizer makes through React context. The house
    conventions say to add a data-testid in the same change; that is deliberately
    not done here, because the target environment serves a prebuilt frontend
    image, so a testid added in this PR could not have been exercised by the run
    above. A data-testid="page-body-scroll-container" would be a strict
    improvement and the POM helper should switch to it.
  2. The exploration flagged a possible gap in the gate, which these specs do not
    assert.
    TracesSpansTab.tsx builds one virtualization object for both
    axes and the comment says one decision drives both, but the column axis was
    observed not to engage on a wide table with few columns. That is an
    optimisation gap, not a correctness bug — nothing renders wrongly — so no
    assertion here pins the current behaviour. Worth an author sanity check
    independently of this PR.

What was deliberately not written

One candidate of three was dropped: select-all and bulk delete covering rows
outside the rendered window
(traces.delete-traces). It verified by hand, but
traces.delete-traces is already covered by trace-delete.spec.ts, and the
right shape is probably to raise that spec's seed count past the windowing
threshold rather than add a second delete spec — a call for whoever owns that
file, not for a generated PR.

Verification, in full

Run from tests_end_to_end/e2e/, against OPIK_BASE_URL=https://pr-7947.dev.comet.com
(OPIK_DEPLOYMENT=oss, workspace default):

npx tsc --noEmit                                                    # clean
python3 tests_end_to_end/coverage/tag_lint.py \
  --taxonomy tests_end_to_end/coverage/taxonomy.yaml \
  --estate tests_end_to_end                                         # 44 specs, 0 problems
npx playwright test tests/trace-explore/traces-table-virtualization.spec.ts
                                                                    # 2 passed
npx playwright test tests/trace-explore/traces-table-virtualization.spec.ts --repeat-each=3
                                                                    # 6 passed
npx playwright test tests/trace-explore/                            # 20 passed

The whole trace-explore/ directory was run because this change touches a shared
POM, a shared fixture chain and the shared backend client.

andriidudar and others added 6 commits August 24, 2026 09:09
A project with a large feedback-score taxonomy gives the traces table one
column per score name; with enough columns and rows this blocks the main
thread for seconds on every interaction. DataTable now renders only the
horizontally visible center columns plus both pinned blocks, with leading and
trailing spacer cells carrying the summed width of what was skipped, so total
width, minWidth and the scrollbar stay unchanged. Row virtualization is
similarly capped so it only activates once a table is actually large.

Both axes are opt-in per table via columnVirtualization / rowVirtualization
props on DataTable, following the same pattern as TableBody={DataTableVirtualBody}.
TracesSpansTab enables both together once the table exceeds 50 columns and 25
rows.

Also works around a backend issue (OPIK_8056) where the traces list endpoint
returns duplicate rows for traces that belong to more than one experiment,
which otherwise produced React key warnings; rows are de-duplicated by id
until the endpoint is fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both axes shared one enabled flag gated by columns > 50 AND rows >= 25. A
table with 1000 columns and 10 rows never crossed the row floor, so the whole
combined condition stayed off and rendered every column uncapped — the axis
that actually needed windowing was blocked by the other axis's count.

Each axis now decides on its own count, or on total cell count (columns ×
rows) crossing the same budget the two thresholds implied together, so a
lopsided table windows the axis that needs it regardless of the other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses PR review comment on columnWindow.ts.
…tion

Raise the cell-count budget from 1250 to 3000 so the default view
(~15 columns x 100 rows = 1500) stays fully rendered, and collapse the
two independently-computed axis flags into one shared enabled decision
so the table is windowed or not as a whole, rather than deciding each
axis separately.
Addresses PR review: DataTableSkeletonBody recomputed
getVisibleLeafColumns()/sliceColumnWindow() every render; memoize it.
Also adds empty-input and out-of-range-index test cases for
sliceColumnWindow.
…lumn windows

Two specs proposed by the release QA side flow from exploratory testing of
#7947, which virtualizes the traces table's rows and columns.

- Row window: 60 seeded traces, swept top to bottom, asserting the rendered
  rows are a contiguous slice of the API's ordering at every offset, no id in
  the DOM twice, and every trace seen exactly once across the sweep.
- Column window: 45 distinct feedback-score names put the table past 60
  columns; with every column turned on from the Columns picker, each rendered
  row's cell sequence and the colgroup must match the header position for
  position at five horizontal offsets, and the pinned select column must stay
  flush with the container's left edge.

Adds a `windowedTraces` / `wideTracesTable` fixture pair (batch REST seeding,
explicit trace teardown), the window-reading helpers on LogsPage, and batch
trace/feedback-score writes on the backend client. Taxonomy updated:
traces.configure-columns flips to covered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added tests Including test files, or tests related like configuration. typescript *.ts *.tsx labels Aug 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📋 PR Linter Failed

Missing Section. The description is missing the ## Details section.


Missing Section. The description is missing the ## Change checklist section.


Missing Section. The description is missing the ## Issues section.


Missing Section. The description is missing the ## Testing section.


Missing Section. The description is missing the ## Documentation section.

@github-actions

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

No linted files changed — nothing to run.

⏭️ 43 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 ⏭️
☕ spotless — java backend Format Java code ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🧪 rebaseline script tests Self-test the changelog re-baseline script ⏭️
🌐 eslint — frontend Lint + autofix JS/TS ⏭️
🌐 typecheck — frontend Whole-project tsc type check ⏭️
📘 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 on lines +26 to +27
'Scrolling a windowed traces table renders every seeded trace exactly once, in the API order',
{ tag: ['@cap:traces.list-traces'] },

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.

Misleading test title overstates coverage

The test title claims each seeded trace renders exactly once, but its assertions only enforce per-window uniqueness and seen.keys() coverage, so overlapping sampled windows can still render a trace multiple times — should we rename it to reflect coverage/order or add a cross-window count assertion?

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
`tests_end_to_end/e2e/tests/trace-explore/traces-table-virtualization.spec.ts` around
lines 26-27, rename the traces row virtualization test to describe that scrolling covers
every seeded trace in API order. The current “exactly once” wording is inaccurate
because overlapping windows are allowed and the assertions never require each trace’s
cross-window count to equal one; preserve the existing coverage, uniqueness-per-window,
and ordering checks.

Comment on lines +151 to +159
const header = await logs.readHeaderColumnSequence();
const rows = await logs.readRenderedRowColumnSequences();
const colgroup = await logs.countColgroupColumns();

expect(rows.length, `scrollLeft=${left}: no rows rendered`).toBeGreaterThan(0);
expect(
colgroup,
`scrollLeft=${left}: the colgroup declares a different number of columns than the header renders`,
).toBe(header.length);

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.

Wrong spacer widths pass alignment test

The column-window assertion ignores spacer and rendered-column widths across the header, body, and colgroup, so a nonzero-width aria-hidden spacer can preserve the __spacer position while shifting cells under the wrong headers and still pass — should we expose and assert each width at every sampled offset?

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
`tests_end_to_end/e2e/tests/trace-explore/traces-table-virtualization.spec.ts` around
lines 151-159, strengthen the `Check header, cell and colgroup alignment across the
width` assertions: comparing normalized sequences and column counts does not detect
incorrect spacer or rendered-column widths. Expose the rendered spacer/column widths
through the relevant `LogsPage` helpers, then assert at every sampled scroll offset that
corresponding header, body-cell, spacer, and `<col>` widths match, including the
`aria-hidden` spacer’s width.

Comment on lines +519 to +530
async readHeaderColumnSequence(): Promise<string[]> {
return this.page
.locator('thead tr th')
.evaluateAll(
(cells, spacer) => cells.map((cell) => cell.getAttribute('data-header-id') ?? spacer),
LogsPage.COLUMN_SPACER,
);
}

/**
* Every rendered row's column sequence, in the same encoding as
* `readHeaderColumnSequence` — so the two are directly comparable, which is

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.

Repeated table checks lack trace steps

readHeaderColumnSequence() and readRenderedRowColumnSequences() call locator evaluateAll directly, so failures from repeated offset checks or scrollTableTo()'s expect.poll samples are unlabeled — should we wrap each body in a descriptive test.step(...) and return that callback, as .agents/skills/writing-e2e-tests/SKILL.md and conventions.md require?

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
tests_end_to_end/e2e/pom/logs.page.ts around lines 519-525, and in the adjacent
readRenderedRowColumnSequences method through line 549, update both table-sequence
helpers to follow the page-object test-step convention. Wrap each locator evaluateAll
operation in a descriptive test.step callback and return the callback, using labels that
identify whether the header or rendered-row column sequence check failed.

Comment on lines +600 to +604
rows: Array.from(document.querySelectorAll('tr[data-row-id]')).flatMap((row) => {
const traceId = row.getAttribute('data-row-id') ?? '';
const cell = row.querySelector(`[data-cell-id="${traceId}_select"]`);
return cell ? [{ traceId, ...describe(cell) }] : [];
}),

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.

Missing pinned cells pass silently

readPinnedSelectColumnGeometry uses flatMap to drop rendered tr[data-row-id] rows when [data-cell-id="${traceId}_select"] is missing, so geometry.rows.length > 0 can pass while the maximum-scroll pinned-column check skips the missing cell — should we return a record/error for every rendered row and assert its cardinality against the rendered trace rows?

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
`tests_end_to_end/e2e/pom/logs.page.ts` around lines 600-604, fix
`readPinnedSelectColumnGeometry` so it does not use `flatMap` to silently discard
rendered rows whose `select` cell is missing. Return a geometry record or explicit
missing-cell error for every `tr[data-row-id]`, and update the associated validation to
assert that the returned row count matches the rendered trace-row count before checking
pinned geometry.

Comment on lines +159 to +165
if (!shouldLeaveArtifacts(testInfo)) {
try {
await backendClient.deleteTraces(orderedIds);
} catch (err) {
console.warn(`[windowedTraces fixture] delete warning for ${project.name}:`, err);
}
}

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.

Setup failures leak persisted traces

When createTracesBatch partially succeeds or waitForTracesListed fails, execution skips use(ref) and leaves accepted trace IDs persisted — should we use a failure-safe try/finally to track partial batches, catch/log cleanup errors, and add an E2E regression test for both paths?

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
tests_end_to_end/e2e/fixtures/windowed-traces.fixture.ts around lines 159-165, refactor
the `windowedTraces` fixture setup, `await use(ref)`, and teardown into a failure-safe
`try/finally`. Track every trace ID successfully submitted, including IDs from partially
accepted batch chunks, and delete those IDs in the `finally` block while catching and
logging cleanup errors so the original setup or test failure remains reported. Add or
update an E2E regression test to force batch or polling setup failure and verify
accepted traces are removed.

Base automatically changed from andriid/OPIK-8021-traces-table-columns-perf to main August 24, 2026 10:14
renderRow={renderRow}
renderNoData={renderNoData}
showLoadingOverlay={showLoadingOverlay}
rowVirtualization={rowVirtualization}

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.

Default tables ignore row virtualization

When TableBody is the default DataTableBody, it accepts rowVirtualization but always renders table.getRowModel().rows.map(renderRow), so DataTable callers using rowVirtualization={{ enabled: true }} without DataTableVirtualBody render every row — should we select DataTableVirtualBody automatically?

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/shared/DataTable/DataTable.tsx` around lines 521-527, fix the
`showSkeleton`/`TableBody` rendering logic so `rowVirtualization.enabled` actually
virtualizes rows when the default body is used. Automatically select the
virtualization-capable body (such as `DataTableVirtualBody`) or otherwise route
rendering through it, while preserving any explicitly supplied custom `TableBody`
behavior.

Comment on lines +27 to +28
export const isColumnSpacer = (item: unknown): item is ColumnSpacer =>
(item as ColumnSpacer | null)?.isColumnSpacer === true;

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.

Malformed spacers produce invalid table widths

isColumnSpacer returns true for any non-null value with isColumnSpacer: true, so malformed values reach DataTable and ColumnSpacerCell as invalid cell.id keys or spacer.size widths such as NaNpx/Infinitypx — should we verify an object with a string id and finite numeric size, and add malformed-shape tests?

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/shared/DataTable/columnVirtualization/columnWindow.ts` around
lines 27-28, harden the `isColumnSpacer` type guard so it only returns true for non-null
objects with `isColumnSpacer === true`, a string `id`, and a finite numeric `size`. Add
malformed-shape tests for missing or non-string ids, missing/non-numeric/NaN/Infinity
sizes, and primitive or null inputs, while preserving valid spacer behavior.

Comment on lines +445 to +452
const wrapper = document.querySelector('[data-table-wrapper]');
if (!wrapper) {
throw new Error('LogsPage: no [data-table-wrapper] on the page');
}
let element = wrapper.parentElement;
while (element) {
const { overflowY } = getComputedStyle(element);
if (overflowY === 'auto' || overflowY === 'scroll') return element;

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.

Layout changes break virtualization E2E coverage

The virtualization helpers rely on computed overflowY and raw table selectors, so layout or markup changes can select the wrong element or return no sections while specs miss the intended behavior. Should we add descriptive scroll-container/table data-testids and use scoped getByTestId() queries, as .agents/skills/writing-e2e-tests/SKILL.md and .agents/skills/playwright-pom-discovery/SKILL.md require?

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
`tests_end_to_end/e2e/pom/logs.page.ts` around lines 445-452, update
`scrollContainerHandle` and the related virtualization helpers to use stable frontend
test IDs instead of `[data-table-wrapper]`, computed `overflowY`, and raw
document-structure selectors. Add descriptive `data-testid` hooks to
`PageBodyScrollContainer` and the virtualized table/sections in the frontend change,
then use `getByTestId()` and scope header, row, colgroup, and spacer queries from the
table hook so the checks cannot silently target the wrong element or return empty data
after a markup restyle.

Comment on lines 932 to 934
const rows: Array<Span | Trace> = useMemo(
() => data?.content ?? [],
() => uniqBy(data?.content ?? [], "id"),
[data?.content],

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.

Logs rows keep arbitrary experiment payload

uniqBy(data?.content ?? [], "id") keeps the first row even though TraceDAO can return multiple experiment rows per trace, so the rendered experiment reference is arbitrary — should we make the backend return one canonical row per trace or deduplicate by an explicit authoritative/version rule before building rows?

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/LogsPage/TracesSpansTab/TracesSpansTab.tsx around lines
932-934, the `rows` construction uses `uniqBy` to discard duplicate trace IDs, which can
silently select an arbitrary experiment payload. Fix the underlying `TraceDAO` list
query so its experiments join returns one deterministic, authoritative row per trace
(using an explicit version/order rule or equivalent `LIMIT 1 BY trace_id` logic), then
remove or retain frontend deduplication only as a defensive measure that cannot
determine experiment data. Add or update coverage for traces associated with multiple
experiments to verify the canonical experiment reference is consistently rendered.

Comment on lines +1353 to +1359
const virtualization = useMemo(
() => ({
enabled:
(columns.length >= 15 || rows.length >= 15) &&
(columns.length > 50 || rows.length > 25 || cellCount > 3000),
}),
[columns.length, rows.length, cellCount],

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.

The virtualization gate hardcodes thresholds inline, so its performance policy is hard to discover and tune consistently — should we extract them into named constants or a virtualizationConfig object with brief rationale for each limit?

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/LogsPage/TracesSpansTab/TracesSpansTab.tsx around lines
1353-1359, the table virtualization policy uses magic thresholds for axis sizes, column
count, row count, and total cell count (15, 50, 25, 3000). Extract these into
descriptive module-level constants (e.g. MIN_VIRTUALIZATION_AXIS_SIZE,
COLUMN_VIRTUALIZATION_THRESHOLD, ROW_VIRTUALIZATION_THRESHOLD,
MAX_VIRTUALIZED_CELL_COUNT) or a single virtualizationConfig object, with brief comments
explaining the rationale for each limit. Update the virtualization condition(s)
controlling columnVirtualization and rowVirtualization to reference these constants
without changing current behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

baz: pending 🔴 size/XL 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.

2 participants