Skip to content

fix(otelMiddleware): open iteration span for structured-output finalization - #1055

Merged
AlemTuzlak merged 2 commits into
mainfrom
1054-otelmiddleware-structured-output-calls-with-no-tools-emit-no-iteration-span-no-generation-record-and-capturecontent-is-a-silent-no-op
Aug 6, 2026
Merged

fix(otelMiddleware): open iteration span for structured-output finalization#1055
AlemTuzlak merged 2 commits into
mainfrom
1054-otelmiddleware-structured-output-calls-with-no-tools-emit-no-iteration-span-no-generation-record-and-capturecontent-is-a-silent-no-op

Conversation

@tombeckenham

@tombeckenham tombeckenham commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • otelMiddleware.onConfig now opens an iteration (CLIENT) span for phase === 'structuredOutput', not only beforeModel.
  • Fixes no-tools + outputSchema runs that skip the agent loop: they previously emitted only a root chat span, so captureContent was a silent no-op and backends keyed on generation spans (e.g. PostHog $ai_generation) saw empty traces.
  • Numbers iteration spans by the middleware’s own model-call counter so a finalization after an agent-loop turn is #N+1, not a duplicate #0.
  • Native-combined mode is unchanged (it never fires structuredOutput).

Closes #1054

Test plan

  • Unit: packages/ai tests/middlewares/otel.test.ts (52 tests) — structuredOutput opens span + content; non-model phases do not; finalization after beforeModel is #1
  • E2E: middleware.spec.ts — Claude 3.7 legacy finalization path asserts 1 CLIENT span + gen_ai.input.messages
  • Docs: docs/advanced/otel.md notes structured-output finalization spans
  • CI green on this PR

Summary by CodeRabbit

  • New Features

    • OpenTelemetry tracing now captures structured-output finalization as a distinct model iteration.
    • Provider model calls include generation records and captured content for improved observability.
    • Iteration numbering now follows the actual order of observed model calls, including structured-output workflows.
  • Documentation

    • Updated OpenTelemetry guidance to explain tracing behavior across agent turns, tool usage, and native combined mode.

…zation

No-tools + outputSchema skips the agent loop, so only phase=structuredOutput
ran and otelMiddleware never opened a generation span — captureContent was a
silent no-op and backends like PostHog saw bare traces. Treat structuredOutput
as a model call for span lifecycle; keep native-combined single-span behavior.

Closes #1054
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

otelMiddleware now creates iteration spans for structured-output finalization and numbers spans by model-call count. Unit and end-to-end tests validate content, usage, phase filtering, and numbering. Documentation and the changeset describe the behavior.

Changes

Structured-output observability

Layer / File(s) Summary
Structured-output iteration span handling
packages/ai/src/middlewares/otel.ts
otelMiddleware processes structuredOutput phases and assigns iteration metadata from its model-call counter.
Iteration span test coverage
packages/ai/tests/middlewares/otel.test.ts, testing/e2e/tests/middleware.spec.ts
Tests validate span creation, captured content, usage, excluded phases, sequential numbering, completion, and metadata.
Documentation and release metadata
docs/advanced/otel.md, docs/config.json, .changeset/otel-structured-output-iteration-span.md
Documentation and release metadata describe structured-output iteration spans and updated iteration numbering.
Estimated code review effort: 3 (Moderate) ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StructuredOutputFinalization
  participant otelMiddleware
  participant ProviderModel
  participant IterationSpan
  StructuredOutputFinalization->>otelMiddleware: onConfig phase structuredOutput
  otelMiddleware->>IterationSpan: create numbered iteration span
  StructuredOutputFinalization->>ProviderModel: send structured-output request
  ProviderModel-->>IterationSpan: return content and usage
Loading

Suggested reviewers: alemtuzlak

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary fix: opening an iteration span during structured-output finalization.
Description check ✅ Passed The description explains the change, motivation, linked issue, tests, documentation, and release-related changes, although it does not use every template heading.
Linked Issues check ✅ Passed The implementation satisfies issue #1054 by adding finalization spans, preserving numbering and native-combined behavior, and restoring content and generation observability.
Out of Scope Changes check ✅ Passed The code, tests, documentation, configuration date, and changeset all directly support the linked issue and stated pull request objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 1054-otelmiddleware-structured-output-calls-with-no-tools-emit-no-iteration-span-no-generation-record-and-capturecontent-is-a-silent-no-op

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Aug 6, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit be444a8

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 14s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-06 09:48:42 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 6, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@1055

@tanstack/ai-acp

npm i https://pkg.pr.new/@tanstack/ai-acp@1055

@tanstack/ai-angular

npm i https://pkg.pr.new/@tanstack/ai-angular@1055

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@1055

@tanstack/ai-bedrock

npm i https://pkg.pr.new/@tanstack/ai-bedrock@1055

@tanstack/ai-byteplus

npm i https://pkg.pr.new/@tanstack/ai-byteplus@1055

@tanstack/ai-claude-code

npm i https://pkg.pr.new/@tanstack/ai-claude-code@1055

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@1055

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@1055

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@1055

@tanstack/ai-codex

npm i https://pkg.pr.new/@tanstack/ai-codex@1055

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@1055

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/@tanstack/ai-durable-stream@1055

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@1055

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@1055

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@1055

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@1055

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@1055

@tanstack/ai-grok-build

npm i https://pkg.pr.new/@tanstack/ai-grok-build@1055

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@1055

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@1055

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@1055

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@1055

@tanstack/ai-mcp

npm i https://pkg.pr.new/@tanstack/ai-mcp@1055

@tanstack/ai-memory

npm i https://pkg.pr.new/@tanstack/ai-memory@1055

@tanstack/ai-mistral

npm i https://pkg.pr.new/@tanstack/ai-mistral@1055

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@1055

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@1055

@tanstack/ai-opencode

npm i https://pkg.pr.new/@tanstack/ai-opencode@1055

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@1055

@tanstack/ai-persistence

npm i https://pkg.pr.new/@tanstack/ai-persistence@1055

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@1055

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@1055

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@1055

@tanstack/ai-sandbox

npm i https://pkg.pr.new/@tanstack/ai-sandbox@1055

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-sandbox-cloudflare@1055

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/@tanstack/ai-sandbox-daytona@1055

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/@tanstack/ai-sandbox-docker@1055

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/@tanstack/ai-sandbox-local-process@1055

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/@tanstack/ai-sandbox-sprites@1055

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/@tanstack/ai-sandbox-vercel@1055

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@1055

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@1055

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@1055

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@1055

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@1055

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@1055

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@1055

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@1055

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@1055

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@1055

commit: be444a8

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@testing/e2e/tests/middleware.spec.ts`:
- Around line 244-246: Update the assertions around inputMessages in the
middleware test to parse the serialized gen_ai.input.messages value as JSON and
verify that the resulting collection contains at least one message with a
content field. Replace the length-only check while preserving the existing
string-type validation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03837be8-a4f0-481b-b0dd-4b261da21780

📥 Commits

Reviewing files that changed from the base of the PR and between aade077 and be444a8.

📒 Files selected for processing (6)
  • .changeset/otel-structured-output-iteration-span.md
  • docs/advanced/otel.md
  • docs/config.json
  • packages/ai/src/middlewares/otel.ts
  • packages/ai/tests/middlewares/otel.test.ts
  • testing/e2e/tests/middleware.spec.ts

Comment on lines +244 to +246
const inputMessages = iter.attributes['gen_ai.input.messages']
expect(typeof inputMessages).toBe('string')
expect(inputMessages.length).toBeGreaterThan(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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert a parsed input message.

inputMessages.length passes for '[]'. The test can pass when no prompt content was captured. Parse the value and assert that it contains a message with content.

Proposed test assertion
 const inputMessages = iter.attributes['gen_ai.input.messages']
 expect(typeof inputMessages).toBe('string')
-expect(inputMessages.length).toBeGreaterThan(0)
+expect(JSON.parse(inputMessages)).toEqual(
+  expect.arrayContaining([
+    expect.objectContaining({ content: expect.any(String) }),
+  ]),
+)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const inputMessages = iter.attributes['gen_ai.input.messages']
expect(typeof inputMessages).toBe('string')
expect(inputMessages.length).toBeGreaterThan(0)
const inputMessages = iter.attributes['gen_ai.input.messages']
expect(typeof inputMessages).toBe('string')
expect(JSON.parse(inputMessages)).toEqual(
expect.arrayContaining([
expect.objectContaining({ content: expect.any(String) }),
]),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@testing/e2e/tests/middleware.spec.ts` around lines 244 - 246, Update the
assertions around inputMessages in the middleware test to parse the serialized
gen_ai.input.messages value as JSON and verify that the resulting collection
contains at least one message with a content field. Replace the length-only
check while preserving the existing string-type validation.

@tombeckenham
tombeckenham requested a review from AlemTuzlak August 6, 2026 09:53
@AlemTuzlak
AlemTuzlak merged commit ed44467 into main Aug 6, 2026
10 of 11 checks passed
@AlemTuzlak
AlemTuzlak deleted the 1054-otelmiddleware-structured-output-calls-with-no-tools-emit-no-iteration-span-no-generation-record-and-capturecontent-is-a-silent-no-op branch August 6, 2026 11:21
@github-actions github-actions Bot mentioned this pull request Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

otelMiddleware: structured-output calls with no tools emit no iteration span — no generation record and captureContent is a silent no-op

2 participants