Skip to content

[OPIK-7717] [QA] Proposed e2e specs for the OTel semconv provider aliases (from the #7909 exploration) - #7945

Draft
CometActions wants to merge 1 commit into
OPIK-7717-otel-provider-aliasesfrom
comet-qa-bot/OPIK-7717/otel-provider-e2e-specs
Draft

[OPIK-7717] [QA] Proposed e2e specs for the OTel semconv provider aliases (from the #7909 exploration)#7945
CometActions wants to merge 1 commit into
OPIK-7717-otel-provider-aliasesfrom
comet-qa-bot/OPIK-7717/otel-provider-e2e-specs

Conversation

@CometActions

@CometActions CometActions commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

What this is

Two proposed e2e specs covering the OTel provider-alias behaviour shipped by
#7909 ([OPIK-7717] [BE] fix: map OTel semconv provider names to Opik canonical providers).

They came out of an exploratory pass over #7909 on its own deployed environment
(pr-7909.dev.comet.com, 2.2.36-7909-merge-3030). A human drove each flow by
hand there first and confirmed it works; only verified flows were turned into
specs.

Base branch: OPIK-7717-otel-provider-aliases, not main. These specs
assert behaviour that exists only in #7909anthropic_vertexai on a
Claude-on-Vertex span, and a non-zero cost for it — so they would fail on main
until that PR merges. They were written and run against its head commit
e377b41. When #7909 merges, this PR retargets to main cleanly.

The specs

Both live in tests_end_to_end/e2e/tests/trace-explore/otel-span-provider.spec.ts,
tagged @t2-cuj / @area:traces.

1. An OTLP-ingested Claude-on-Vertex span resolves to anthropic_vertexai and is priced from the Anthropic rows

@cap:traces.span-model-cost-tokens

Pushes two spans through POST /v1/private/otel/v1/traces with identical token
counts (1000 in / 500 out) — gen_ai.system=vertex_ai with claude-haiku-4-5,
and the same provider with gemini-2.5-flash-lite — then asserts:

  • via GET /v1/private/spans: provider anthropic_vertexai at $0.0035 for
    the Claude span and google_vertexai at $0.0003 for the Gemini one, over
    the project's whole span set (count included), not just the rows it looked up;
  • via the page: the span-detail chip reads anthropic_vertexai claude-haiku-4-5
    and the cost renders <$0.01.

The exact costs are the point. Same tokens on both spans means only the resolved
provider can move the number, and the pre-fix behaviour — a Claude model left
under google_vertexai matching no price row — renders a perfectly ordinary span
that costs $0. trace-spans-depth.spec.ts already covers this capability, but
only through the Python SDK with a provider the test itself chose, so it cannot
fail on the OTel mapping.

Verification: passed.

2. The Spans tab provider filter matches OTLP spans under their canonical provider name

@cap:traces.toggle-spans-view — previously covered: false in the taxonomy
("Logs has a 3rd toggle (Spans); only Traces+Threads covered").

Seeds three OTLP spans (Claude via gen_ai.system, Gemini via gen_ai.system,
Gemini via the semconv-current gen_ai.provider.name=gcp.vertex_ai), confirms
via the API that they really split 1/2 across the two providers, then clicks the
Spans toggle and filters:

  • provider contains anthropic_vertexai → exactly the 1 Claude span;
  • provider contains google_vertexai → exactly the other 2.

Each step asserts the row count and which rows survived, so a filter that
returned the right number of wrong rows fails. The gcp.vertex_ai span is the
discriminator for the attribute half: a chain still reading gen_ai.system
alone would leave it with no provider at all.

Verification: passed.

Supporting changes

  • fixtures/otel-spans.fixture.ts — new fixture, chained and re-exported like
    the others. Seeding lives here rather than in a trailing cleanup step, and
    teardown rides on the project fixture's cascade (same as
    filterable-traces.fixture.ts). It polls for the seeded span names after the
    POST — OTLP ingestion answers 200 before the spans are queryable — and throws
    naming what never landed rather than handing the spec a partial set.
  • core/backend/client.tsingestOtelSpans (raw fetch: OTLP has no SDK
    surface, and it is the only path the alias chain runs on), listSpans, and
    waitForSpansByName in core/backend/wait-for-spans.ts.
  • pom/logs.page.ts — Spans-tab support: the toggle, span rows keyed by
    data-row-id, spanCell via data-cell-id, and openSpanById.
  • pom/trace-panel.page.tswaitForSpanSelected, because a panel opened
    straight at ?trace=…&span=… mounts on the trace and swaps to the span a beat
    later.
  • coverage/taxonomy.yaml — spec added to the area's specs: list;
    toggle-spans-view flipped to covered: true, tier: t2-cuj.

One shared-POM fix, worth a look

LogsPage.applyFilter now blurs the value input before closing the popover.
Every filter value cell wraps a DebounceInput whose pending callback is
cancelled on unmount, so closing the chip within the 300ms debounce window
discards the typed value and applies no filter at all. The existing callers hit
this only by luck: they drive autocomplete cells, whose own Escape handler blurs
the input and flushes the debounce first. A plain text cell — which is what the
provider chip is — has no such handler, and the filter silently did nothing.
Blur is the FE's designed flush path (DebounceInput.handleBlur calls
.flush()), and it is the same idiom TracePanelPage.setAnnotateScore already
uses.

The whole tests/trace-explore/ directory was re-run after this change: 19
passed, 1 flaky
trace-filters.spec.ts › Feedback score filter …, which
timed out opening its chip popover and passed on retry.

That one is pre-existing and unrelated: it goes through pinFilterChip +
applyKeyedFilter, neither of which this PR touches, and it fails the same way
with the applyFilter change reverted (run in isolation with --repeat-each=4,
it failed every attempt on both sides of the change). Flagging it because
applyKeyedFilter looks like it has the same un-flushed-debounce problem
applyFilter had — worth a separate fix, not folded in here.

What I deliberately did not write

One candidate from the exploration was dropped: "a fully-migrated
execute_tool span carrying gen_ai.provider.name is not retyped as an LLM
call."
The exploration ranked it weak on the grounds that it overlaps the
first spec's seeding and reads more naturally as one more assertion there than
as a spec of its own. It verified fine by hand (type=tool, no cost, Logs
renders Tool) and it does guard a real regression this PR's review feedback
caught, so it is a reasonable follow-up — it is left out because the ranking said
weak and there were stronger candidates, not because it failed.

Nothing was dropped for failing to run.

How this was verified

cd tests_end_to_end/e2e
npx playwright test tests/trace-explore/otel-span-provider.spec.ts --reporter=list
npx playwright test tests/trace-explore/ --reporter=list          # shared-POM regression check
npx tsc --noEmit
python3 tests_end_to_end/coverage/tag_lint.py \
  --taxonomy tests_end_to_end/coverage/taxonomy.yaml --estate tests_end_to_end

Target: an OSS install of the #7909 build (OPIK_DEPLOYMENT=oss, workspace
default). tsc clean; tag-lint 0 problem(s).


🤖 Generated by the release QA side flow (release-test-proposal) from a
human-verified exploration of #7909. Draft on purpose — it needs a human
review before merge.
The assertions are only as good as the exploration they
came from; please check that each @cap: tag matches what the test actually
asserts.

Two proposed specs covering the provider-alias behaviour from #7909,
written from a human-verified exploration of that PR's own environment.

- traces.span-model-cost-tokens: an OTLP-ingested Claude-on-Vertex span
  resolves to anthropic_vertexai and prices from the Anthropic rows
  ($0.0035), while an identically-tokened Gemini span prices from the
  Vertex language-model rows ($0.0003). Asserted via GET /v1/private/spans
  and on the span-detail chip.
- traces.toggle-spans-view (was covered: false): the Spans tab's provider
  filter matches OTLP spans under their canonical provider name, 1 vs 2
  rows across the two providers.

Supporting: an otelSpans fixture seeding through the OTLP endpoint,
ingestOtelSpans/listSpans/waitForSpansByName on the backend client,
Spans-tab support on LogsPage, and waitForSpanSelected on TracePanelPage.

LogsPage.applyFilter now blurs the value input before closing the chip:
the value cells wrap a DebounceInput whose pending callback is cancelled
on unmount, so closing within the 300ms window applied no filter at all.
The existing callers only avoided this because the autocomplete cells
blur themselves on Escape; the plain-text provider chip does not.

Generated by the release QA side flow — draft, needs review before merge.

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 21, 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 +314 to +315
id: String(s.id ?? ''),
traceId: String(s.traceId ?? ''),

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 span IDs reach navigation

The mapper converts omitted SpanPublic.id and traceId into '' in SpanRowRef, while waitForSpansByName accepts the row by name alone and copies those values into OtelSpanRef, so openSpanById builds selectors/URLs from empty values and the detail assertion targets no row instead of rejecting the malformed response. Although the current spans table normally stores non-null FixedString IDs, should we require non-empty id and traceId during mapping—or in the waiter’s success predicate—since the published Span_Public schema does not require them?

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/core/backend/client.ts around lines 314-315, update the
`localListSpans` mapper so optional SDK `id` and `traceId` fields are not coerced to
empty strings in `SpanRowRef`. Validate that both values are non-empty before returning
each span, and throw a descriptive error for malformed backend responses (optionally
also require both fields in the `waitForSpansByName` success predicate). Add or update
coverage to ensure spans with missing identity fields are rejected at the client
boundary.

Comment on lines +623 to +629
const toAttribute = (key: string, value: string | number) => ({
key,
// OTLP/JSON encodes 64-bit ints as strings; the token-usage attributes
// are the ones that must arrive as ints or the price lookup sees none.
value:
typeof value === 'number' ? { intValue: String(value) } : { stringValue: value },
});

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.

Non-integer attributes reject whole batches

The public OtelSpanSeed.attributes contract accepts any JavaScript number, but this branch encodes every value as protobuf int64 intValue, so 1.5, NaN, and Infinity are rejected by the backend's JsonFormat decoder and the entire OTLP request fails before any span is stored. Could we restrict or validate integer attributes with Number.isFinite(value) && Number.isInteger(value), or emit finite non-integers as doubleValue?

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/core/backend/client.ts around lines 623-629, update the
`ingestOtelSpans` `toAttribute` helper so its handling matches the public
`OtelSpanSeed.attributes` contract. Reject non-finite numeric values with a clear error
before building the request, encode integers as OTLP `intValue`, and encode finite
non-integers as `doubleValue`; preserve string attributes as `stringValue` and update
nearby comments/types if needed.

Comment on lines +640 to +642
spans: args.spans.map((span) => ({
traceId: span.traceId,
spanId: span.spanId,

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.

OTLP IDs are remapped to wrong identities

OtelSpanSeed sends 32/16-character hex traceId/spanId values straight to the protobuf JsonFormat reader, which treats them as base64 and decodes them to 24/12 bytes, and OpenTelemetryService still returns 200 for these — so persisted Opik IDs no longer match the supplied OTLP identities and !res.ok can't catch it. Should we hex-decode the IDs into 16/8 raw bytes and base64-encode those for the payload, validating lengths/characters first?

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/core/backend/client.ts around lines 640-642, fix `ingestOtelSpans`
so the OTLP `traceId` and `spanId` values are not sent as raw hex strings to the
protobuf JSON reader, which interprets them as base64 and produces incorrectly sized
IDs. Validate that each ID is hexadecimal with exactly 32 characters for `traceId` and
16 for `spanId`, decode the hex into 16/8 raw bytes, and base64-encode those bytes for
the request payload; throw a clear error for invalid lengths or characters.

Comment on lines +674 to +677
if (!res.ok) {
throw new Error(
`POST /v1/private/otel/v1/traces -> ${res.status}: ${(await res.text()).slice(0, 300)}`,
);

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.

Unsanitized OTLP errors leak into CI output

The thrown Playwright error interpolates the first 300 characters of the remote response verbatim, so parser-failure ErrorMessage bodies built from exception.getMessage() (including caller payload details) can inject newlines or expose sensitive content in CI artifacts when OPIK_BASE_URL targets cloud or self-hosted deployments. Should we retain the 300-character bound but sanitize to a single-line safe excerpt and mask approved secret/token patterns before interpolation?

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/core/backend/client.ts` around lines 674-677, update
`ingestOtelSpans` so failed OTLP requests do not interpolate the remote response body
verbatim into the Playwright error. Read the body once, create a single-line safe
excerpt limited to 300 characters, replace or escape control characters, and mask
approved secret/token patterns before including it in the error message. Preserve the
status and existing error behavior.

Comment on lines +35 to +37
while (Date.now() - start < timeoutMs) {
const { spans } = await listSpans({ projectId });
seen = spans.map((s) => s.name);

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.

Transient ingestion failure aborts polling

A transient listSpans failure makes waitForSpansByName reject immediately, so asynchronous OTLP ingestion cannot recover within the polling window and surfaces as a startup/readiness failure — should we retain intermediate errors, continue polling until timeoutMs, and include the last error's message/String(error) in the final timeout error?

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/core/backend/wait-for-spans.ts` around lines 35-37, update
`waitForSpansByName` so a transient `listSpans` error does not reject the function
immediately. Catch each polling error, retain the most recent error while continuing to
poll until `timeoutMs`, and include `String(error)` or its message in the final timeout
error alongside the missing and present spans.

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.

1 participant