Skip to content

fix(tracing): close four silent gaps in Phoenix span coverage - #263

Open
NickB03 wants to merge 9 commits into
mainfrom
tracing/phoenix-span-coverage
Open

fix(tracing): close four silent gaps in Phoenix span coverage#263
NickB03 wants to merge 9 commits into
mainfrom
tracing/phoenix-span-coverage

Conversation

@NickB03

@NickB03 NickB03 commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Why

Four independent gaps in Phoenix tracing, all of which fail silently — which is why none of them had been noticed.

What changed

1. Aborted chats lost every span. onFinish returned early on abort, before await flushTraces(). On Vercel the function terminates as soon as the response settles, so an aborted chat's spans died unexported. Aborted chats are exactly the traces worth having — they are where users bail out. The flush now runs in a finally; persistence still skips aborted and empty responses. Regression tests cover the abort path, the missing-responseMessage path, and the case where persistence throws.

2. OPENINFERENCE_HIDE_* masked nothing. The vars were documented as hiding prompt and completion content, but nothing in this setup reads them — openinference-vercel's span processor doesn't consume them here. An operator could set them, see no error, and keep shipping every user prompt to Phoenix. The AI SDK does honour per-call recordInputs/recordOutputs, so a telemetryRecordingOptions() helper is now spread into every experimental_telemetry block and is the single enforcement point. Five vars that will never be honoured (including the non-HIDE_-prefixed OPENINFERENCE_BASE64_IMAGE_MAX_LENGTH) are deleted from both lists in lib/config/env.ts and from the docs — leaving them documented is the exact failure this fixes.

3. Two LLM calls emitted no spans at all. The app makes five LLM calls; three were instrumented. Image generation and trending-suggestions were invisible in Phoenix — including the trending-suggestions call that runs on a Vercel cron, where there is no UI for anyone to notice a failure. All five call sites are now instrumented and uniform.

4. A blind deploy looked healthy. instrumentation.ts deliberately disables tracing when the collector endpoint is plain HTTP in production. It did so silently, and /api/health reported phoenix: 'ok' because the collector was reachable — while this process had registered no exporter. /api/health now also returns tracing, so the blind-deploy signature phoenix: 'ok' + tracing: 'disabled-https' is visible.

Verification

bun lint, bun typecheck, and the full root suite all clean on this branch standalone: 188 test files, 1572 tests, 0 failures.

⚠️ Read before deploying

This change makes OPENINFERENCE_HIDE_INPUTS / OPENINFERENCE_HIDE_OUTPUTS work for the first time. If either is already set to true in Vercel production — plausibly set at some point in the belief that it was already masking — merging this will silently blank prompt and completion content on every AI SDK span in Phoenix. Check the current values first. That is a decision to make deliberately, not to discover in the trace viewer.

Setting any of the five deleted vars after this merge is inert, exactly as it was before.

Runtime notes

  • flushTraces() cannot throw or hang past 5s: it early-returns when tracing is off, wraps its body in a try/catch that only warns, and races forceFlush() against a timer that resolves rather than rejects. Moving it into finally cannot convert a persistence failure into an unhandled rejection.
  • globalThis.__polymorphTracingState is written once during register() at process init and read-only thereafter, so there is no concurrent-write hazard. Each serverless instance sets its own — which is the correct semantics, since the field means "did this process register an exporter." Consequence worth knowing: /api/health?check=phoenix reports whichever instance answered, so one enabled response does not prove every instance is exporting. Sample a few times when diagnosing.

Known follow-ups (not blocking)

  • The enabled and init-failed tracing states have no test coverage; reaching them requires mocking the full OTel stack. The two reachable states (disabled-off, disabled-https) and the whole health-route field are covered, and the disabled-off test is ordering-sensitive — it fails if the default assignment moves after the ENABLE_TRACING check.
  • TracingState is declared identically in instrumentation.ts and app/api/health/route.ts. TypeScript errors if the two drift, so it is self-policing, but a shared export would be tidier.

Review notes

Commits were cherry-picked from the working branch evals/pipeline-restore-and-quality, which also carries an unrelated eval-harness change set. That half ships separately in #262. The two surfaces share no files and no data dependencies.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Health checks now report whether tracing is enabled, disabled, or unavailable.
    • AI telemetry consistently applies input and output masking settings across supported operations.
    • Additional AI activities, including image generation and suggestions, now include tracing details.
  • Bug Fixes

    • Traces are flushed even when chat streams are interrupted or persistence fails.
    • Stream results are no longer persisted when a response is unavailable.
  • Documentation

    • Clarified tracing status responses and production masking behavior.

NickB03 and others added 8 commits July 27, 2026 06:18
onFinish returned early on abort, before flushTraces(), so every aborted
chat's spans died unexported when the serverless function terminated.
Persistence still skips aborted/empty responses; only the flush is now
unconditional.

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

Commit 1e8d68e moved flushTraces() into a finally block so aborted
streams still export their OTel spans, but no test drove onFinish
with isAborted or a missing responseMessage — the exact branch the
fix exists for. Parameterize the ai mock's onFinish payload and add
coverage for the abort path, the missing-responseMessage path, and
persistStreamResults throwing, asserting flushTraces still runs and
persistStreamResults is skipped/isolated as expected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing read these vars in this setup, so operators could set them, see no
error, and still ship prompts to Phoenix. Routes them through a
telemetryRecordingOptions() helper spread into every experimental_telemetry
block, prunes the unhonoured vars from the schema, and corrects the docs.

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

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

Both ran untraced, so their latency and failures never reached Phoenix —
including the trending-suggestions call on the Vercel cron, where there is no
UI to notice a failure.

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

phoenix: 'ok' only proves the collector is reachable, not that this process
registered an exporter. The HTTPS guard could silently disable tracing in
production with the health check still green.

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

Adds regression coverage for the globalThis.__polymorphTracingState
assignments in instrumentation.ts (disabled-off, disabled-https) and
for how /api/health surfaces that global as body.tracing, so a future
refactor that reorders the default assignment or breaks the field
gating is caught instead of silently reintroducing the blind-deploy
visibility gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
STREAMING-OPERATIONS.md said onFinish skipped trace flushing for aborted
responses; it now flushes unconditionally and only persistence is skipped.
ENVIRONMENT-OPERATIONS.md's instrumentation.ts line reference was stale.
API-AUXILIARY-ENDPOINTS.md did not document the new /api/health `tracing`
field at all.

Split out of a combined docs commit on the working branch so this PR carries
only the tracing surface; the evals-side docs ship in the evals PR.

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

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
polymorph Ready Ready Preview, Comment Jul 27, 2026 4:09pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Observability controls and reporting

Layer / File(s) Summary
Telemetry recording controls
lib/utils/telemetry.*, lib/config/env.ts, lib/agents/..., lib/tools/generate-image/..., .env.local.example, docs/...ENVIRONMENT-OPERATIONS.md, docs/...PHOENIX-OPERATIONS.md
Centralizes OpenInference input/output recording options, applies them to LLM telemetry calls, removes obsolete environment variables, and updates masking documentation.
Tracing state and health reporting
instrumentation.*, app/api/health/..., docs/reference/API-AUXILIARY-ENDPOINTS.md
Tracks tracing initialization states globally and exposes them through Phoenix health checks with tests and response documentation.
Stream completion and trace flushing
lib/streaming/..., docs/architecture/STREAMING-OPERATIONS.md
Conditionally persists stream results while always flushing traces, including aborted and persistence-error cases, with corresponding tests and documentation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant register
  participant globalThis
  participant HealthGET
  participant HealthClient
  register->>globalThis: Set __polymorphTracingState
  HealthClient->>HealthGET: Request check=phoenix or check=all
  HealthGET->>globalThis: Read tracing state
  globalThis-->>HealthGET: Return state or unknown
  HealthGET-->>HealthClient: Return tracing metadata
Loading

Possibly related PRs

Suggested reviewers: maintainer

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise, specific, and accurately summarizes the main tracing coverage fix in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 tracing/phoenix-span-coverage

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e5b46d191

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +389 to +390
// traces most worth keeping. Runs after persistence so spans include
// DB write latency. The 5s timeout (default) is small relative to the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop claiming the flush captures persistence latency

For successful chats, withOtelRootSpan finishes at line 355 before onFinish starts, and persistStreamResults runs later at lines 361–378 outside that span. flushTraces() only exports spans that were already recorded; it cannot add the intervening DB-write duration, so Phoenix traces still exclude persistence latency. This new comment—and the matching statement in STREAMING-OPERATIONS.md—will mislead operators diagnosing slow completions; either instrument persistence within an active span or remove the claim.

Useful? React with 👍 / 👎.

The wording introduced earlier in this PR claimed "Root-span and tool-event
attributes are unaffected." Half of that is wrong and the other half reads as
reassurance when it is actually the caveat.

The root span really is unaffected — because `withOtelRootSpan` in
create-chat-stream-response.ts builds it outside the AI SDK, so
telemetryRecordingOptions() structurally cannot reach it. It keeps emitting
session id, user id, and request metadata no matter how the flags are set.
That is a limit to document, not a comfort. Masking removes message content,
not who sent it.

The tool-event half is unverified and was dropped rather than replaced with
another specific claim.

Also documents that only the exact string "true" masks, so the control fails
toward recording rather than toward hiding — the direction that matters for a
privacy switch.

This PR exists because these variables were documented as doing something they
did not do. Shipping a fresh inaccurate claim about them would repeat exactly
that mistake.

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

@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

🧹 Nitpick comments (1)
lib/utils/telemetry.test.ts (1)

154-168: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Restore stubbed environment values in test cleanup.

vi.unstubAllEnvs() runs only after the first assertion succeeds. If an assertion fails, the stubs can leak into later tests and cause cascading failures; move cleanup to afterEach or a finally block.

Proposed fix
 describe('telemetryRecordingOptions', () => {
+  afterEach(() => {
+    vi.unstubAllEnvs()
+  })
+
   it('telemetryRecordingOptions honors OPENINFERENCE_HIDE_INPUTS/OUTPUTS', () => {
     vi.stubEnv('OPENINFERENCE_HIDE_INPUTS', 'true')
     vi.stubEnv('OPENINFERENCE_HIDE_OUTPUTS', 'false')
@@
-    vi.unstubAllEnvs()
     expect(telemetryRecordingOptions()).toEqual({
       recordInputs: true,
       recordOutputs: true
🤖 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 `@lib/utils/telemetry.test.ts` around lines 154 - 168, Move the
vi.unstubAllEnvs cleanup out of the telemetryRecordingOptions test body and into
an afterEach hook for the describe block, ensuring environment stubs are
restored even when an assertion fails. Keep the existing assertions and expected
telemetryRecordingOptions behavior unchanged.
🤖 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 `@app/api/health/route.test.ts`:
- Around line 25-74: Make the GET /api/health tests deterministic by disabling
tracing in beforeEach, or explicitly mocking fetch and the Phoenix collector
endpoint so no test performs a real /healthz request when ENABLE_TRACING and
PHOENIX_COLLECTOR_ENDPOINT are inherited from the environment. Preserve the
existing assertions for global tracing state and response contents.

---

Nitpick comments:
In `@lib/utils/telemetry.test.ts`:
- Around line 154-168: Move the vi.unstubAllEnvs cleanup out of the
telemetryRecordingOptions test body and into an afterEach hook for the describe
block, ensuring environment stubs are restored even when an assertion fails.
Keep the existing assertions and expected telemetryRecordingOptions behavior
unchanged.
🪄 Autofix (Beta)

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: e91ea4b3-9655-476c-a9ec-935ea4be4815

📥 Commits

Reviewing files that changed from the base of the PR and between 321b997 and 62088ce.

📒 Files selected for processing (22)
  • .env.local.example
  • app/api/health/route.test.ts
  • app/api/health/route.ts
  • docs/architecture/STREAMING-OPERATIONS.md
  • docs/getting-started/ENVIRONMENT-OPERATIONS.md
  • docs/operations/PHOENIX-OPERATIONS.md
  • docs/reference/API-AUXILIARY-ENDPOINTS.md
  • instrumentation.test.ts
  • instrumentation.ts
  • lib/agents/__tests__/researcher.test.ts
  • lib/agents/__tests__/title-generator.test.ts
  • lib/agents/chat/__tests__/registry.test.ts
  • lib/agents/chat/factory.ts
  • lib/agents/generate-related-questions.ts
  • lib/agents/generate-trending-suggestions.ts
  • lib/agents/title-generator.ts
  • lib/config/env.ts
  • lib/streaming/__tests__/create-chat-stream-response.test.ts
  • lib/streaming/create-chat-stream-response.ts
  • lib/tools/generate-image/server.ts
  • lib/utils/telemetry.test.ts
  • lib/utils/telemetry.ts
💤 Files with no reviewable changes (1)
  • lib/config/env.ts

Comment on lines +25 to +74
describe('GET /api/health', () => {
it('includes tracing in the body for check=phoenix, reflecting the current global state', async () => {
mockExecute.mockResolvedValue(undefined)
globalThis.__polymorphTracingState = 'disabled-https'

const response = await GET(makeRequest('?check=phoenix'))
const body = await response.json()

expect(body.tracing).toBe('disabled-https')
})

it('includes tracing in the body for check=all, reflecting the current global state', async () => {
mockExecute.mockResolvedValue(undefined)
globalThis.__polymorphTracingState = 'enabled'

const response = await GET(makeRequest('?check=all'))
const body = await response.json()

expect(body.tracing).toBe('enabled')
})

it('omits tracing from the body when check is unset', async () => {
mockExecute.mockResolvedValue(undefined)
globalThis.__polymorphTracingState = 'enabled'

const response = await GET(makeRequest())
const body = await response.json()

expect(body).not.toHaveProperty('tracing')
})

it('omits tracing from the body for an unrecognized check value', async () => {
mockExecute.mockResolvedValue(undefined)
globalThis.__polymorphTracingState = 'enabled'

const response = await GET(makeRequest('?check=db'))
const body = await response.json()

expect(body).not.toHaveProperty('tracing')
})

it('falls back to unknown when the global tracing state was never set', async () => {
mockExecute.mockResolvedValue(undefined)

const response = await GET(makeRequest('?check=phoenix'))
const body = await response.json()

expect(body.tracing).toBe('unknown')
})
})

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make Phoenix-check tests independent of inherited environment.

If CI provides both ENABLE_TRACING=true and PHOENIX_COLLECTOR_ENDPOINT, these tests perform a real /healthz fetch. Stub tracing off in beforeEach, or mock fetch and the endpoint explicitly.

🤖 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 `@app/api/health/route.test.ts` around lines 25 - 74, Make the GET /api/health
tests deterministic by disabling tracing in beforeEach, or explicitly mocking
fetch and the Phoenix collector endpoint so no test performs a real /healthz
request when ENABLE_TRACING and PHOENIX_COLLECTOR_ENDPOINT are inherited from
the environment. Preserve the existing assertions for global tracing state and
response contents.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 62088cef2e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

`[onFinish] Failed to persist stream results for chat ${chatId}:`,
error
)
if (!isAborted && responseMessage) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wait for title generation before flushing aborted traces

When a new chat is aborted while generateChatTitle is still running, this branch skips persistStreamResults, which is the only path that awaits titlePromise, and immediately calls flushTraces(). The flush can therefore finish before the title-generation span ends; once the serverless invocation terminates, that span is still lost. Await the already error-handled titlePromise before flushing aborted streams so the new abort coverage includes this parallel LLM call.

Useful? React with 👍 / 👎.

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.

1 participant