Skip to content

feat: improve OpenTelemetry trace fidelity - #847

Closed
chrisdoc wants to merge 2 commits into
mainfrom
feat/otel-exception-tracking
Closed

feat: improve OpenTelemetry trace fidelity#847
chrisdoc wants to merge 2 commits into
mainfrom
feat/otel-exception-tracking

Conversation

@chrisdoc

@chrisdoc chrisdoc commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • add privacy-safe MCP session correlation across Node spans
  • emit naturally timed Hevy API attempt and retry-wait spans
  • instrument cache states and bounded catalog metadata
  • add safe lifecycle, SDK, discovery, and process failure telemetry
  • normalize transport, resource metadata, and span categories

Closes #845

Validation

  • npm run build
  • npm run test:unit
  • npm run test:stdio
  • npm run check
  • npm run check:types

npm run check:changeset remains blocked by the local checkout's missing origin/main baseline object.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of retries, network failures, delayed responses, and expected missing-page results.
    • Prevented observability or diagnostic failures from interrupting normal requests, caching, or tool execution.
  • Improvements
    • Added more consistent request, retry, cache, and tool lifecycle reporting.
    • Enhanced error classification and sanitized diagnostics for safer troubleshooting.
    • Added privacy-safe session correlation without exposing session identifiers.
  • Developer Experience
    • Exposed additional telemetry and cache-observation types for integrations and monitoring tools.

✨ PR Description

Purpose: Enhance OpenTelemetry trace fidelity by adding exception tracking, cache observation spans, and structured failure events across API, cache, and SDK layers.

Main changes:

  • Implemented cache observation interface with state tracking (hit/miss/refresh/expired) and metadata collection in AsyncTtlCache
  • Added structured MCP tool failure events and request/retry lifecycle spans to hevy-client with outcome classification
  • Integrated exception recording with normalized error types, process failure tracking, and session correlation IDs

Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Description using Guidelines Learn how

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 181e7f38-6f18-4907-af53-5c2da99434c1

📥 Commits

Reviewing files that changed from the base of the PR and between b670f6d and 0ea209d.

📒 Files selected for processing (25)
  • .changeset/opaque-telemetry-correlation.md
  • .changeset/steady-failure-events.md
  • packages/core/src/index.ts
  • packages/core/src/server.ts
  • packages/core/src/utils/cache.test.ts
  • packages/core/src/utils/cache.ts
  • packages/core/src/utils/error-handler.test.ts
  • packages/core/src/utils/exercise-template-catalog.ts
  • packages/core/src/utils/tool-taxonomy.ts
  • packages/hevy-client/src/hevy-client-kubb.ts
  • packages/hevy-client/src/hevy-client.test.ts
  • packages/hevy-client/src/index.ts
  • packages/node/src/index.test.ts
  • packages/node/src/index.ts
  • packages/node/src/utils/hevy-client-observability.test.ts
  • packages/node/src/utils/hevy-client-observability.ts
  • packages/node/src/utils/mcp-session-observability.test.ts
  • packages/node/src/utils/mcp-session-observability.ts
  • packages/node/src/utils/sentry-privacy.test.ts
  • packages/node/src/utils/stdio-observability.test.ts
  • packages/node/src/utils/stdio-observability.ts
  • packages/node/src/utils/telemetry.test.ts
  • packages/node/src/utils/telemetry.ts
  • packages/node/src/utils/tool-observer.test.ts
  • packages/node/src/utils/tool-observer.ts
🚧 Files skipped from review as they are similar to previous changes (24)
  • packages/core/src/utils/error-handler.test.ts
  • .changeset/opaque-telemetry-correlation.md
  • .changeset/steady-failure-events.md
  • packages/node/src/utils/sentry-privacy.test.ts
  • packages/core/src/utils/cache.test.ts
  • packages/node/src/utils/mcp-session-observability.test.ts
  • packages/node/src/utils/telemetry.test.ts
  • packages/node/src/utils/stdio-observability.test.ts
  • packages/node/src/utils/hevy-client-observability.ts
  • packages/hevy-client/src/index.ts
  • packages/core/src/utils/tool-taxonomy.ts
  • packages/node/src/index.test.ts
  • packages/hevy-client/src/hevy-client.test.ts
  • packages/node/src/utils/stdio-observability.ts
  • packages/node/src/utils/hevy-client-observability.test.ts
  • packages/node/src/utils/telemetry.ts
  • packages/node/src/utils/tool-observer.ts
  • packages/node/src/utils/mcp-session-observability.ts
  • packages/hevy-client/src/hevy-client-kubb.ts
  • packages/core/src/server.ts
  • packages/node/src/utils/tool-observer.test.ts
  • packages/core/src/utils/cache.ts
  • packages/core/src/utils/exercise-template-catalog.ts
  • packages/node/src/index.ts

📝 Walkthrough

Walkthrough

The PR adds privacy-safe MCP session correlation, structured Hevy request outcomes, cache lifecycle observation, bounded failure telemetry, span taxonomy, service identity, and related tests across the core, client, and Node packages.

Changes

Telemetry observability

Layer / File(s) Summary
Core observation contracts and cache instrumentation
packages/core/src/utils/cache.ts, packages/core/src/utils/exercise-template-catalog.ts, packages/core/src/server.ts, packages/core/src/index.ts, packages/core/src/utils/tool-taxonomy.ts, packages/core/src/utils/cache.test.ts, packages/core/src/utils/error-handler.test.ts, .changeset/*
The core package exports cache and span taxonomy contracts. Cache hits, misses, refreshes, expirations, and in-flight waits produce bounded observations. Exercise-template loads attach bucketed metadata.
Hevy request lifecycle and outcome classification
packages/hevy-client/src/hevy-client-kubb.ts, packages/hevy-client/src/index.ts, packages/hevy-client/src/hevy-client.test.ts
The client instruments request starts, retries, waits, response parsing, expected 404 outcomes, terminal failures, and sanitized error metadata.
Session correlation and service identity
packages/node/src/utils/mcp-session-observability.ts, packages/node/src/utils/stdio-observability.ts, packages/node/src/utils/telemetry.ts, packages/node/src/utils/sentry-privacy.test.ts, packages/node/src/utils/*test.ts
Node telemetry creates opaque session IDs, propagates them to selected spans, removes them from Sentry data, and records bounded service and exception metadata.
API and cache span integration
packages/node/src/utils/hevy-client-observability.ts, packages/node/src/utils/hevy-client-observability.test.ts
Request and cache observers create spans with outcome, retry, transport, session, refresh, page-count, and item-count attributes.
Tool, protocol, and lifecycle failure integration
packages/node/src/index.ts, packages/node/src/utils/tool-observer.ts, packages/node/src/utils/tool-observer.test.ts, packages/node/src/index.test.ts
Server lifecycle, protocol, discovery, SDK, and tool failures emit bounded diagnostics and canonical failure events. Tool spans use discovery and tool categories with standardized HTTP attributes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant NodeServer
  participant HevyClient
  participant OpenTelemetry
  MCPClient->>NodeServer: initialize or invoke tool
  NodeServer->>OpenTelemetry: create session, protocol, or tool span
  NodeServer->>HevyClient: execute API request
  HevyClient->>OpenTelemetry: report request, retry, and outcome observations
  NodeServer->>OpenTelemetry: finish tool span or record bounded failure
Loading

Possibly related issues

Possibly related PRs

Suggested labels: 30 min review

Suggested reviewers: bhanuprasad14

Poem

I’m a rabbit tracking spans through the hay,
With opaque little IDs that stay out of the way.
Cache hits hop softly; retries wait in line,
Failures wear bounded tags, neat and benign.
Telemetry carrots now grow in a tidy design.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.98% 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 clearly summarizes the PR's primary change: improved OpenTelemetry trace fidelity.
Linked Issues check ✅ Passed The changes implement the telemetry, privacy, failure, caching, metadata, and testing objectives in [#845].
Out of Scope Changes check ✅ Passed The code and tests remain within the observability, privacy, reliability, and validation scope defined in [#845].
📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #845

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/otel-exception-tracking

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.

@gitstream-cm gitstream-cm 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.

✨ PR Review

The PR substantially improves OTel trace fidelity with well-structured observer patterns, privacy-safe session IDs, and bounded metadata. Three issues are worth addressing before merging.

3 issues detected:

🐞 Bug - `startActiveSpan` with an immediately-returning callback creates a span but activates its context only for a zero-duration synchronous frame; the real async request work is never inside that active context.

Details: startApiSpan calls tracer.startActiveSpan with a callback that immediately returns the span ((span) => span). startActiveSpan sets the span as the active context only for the duration of the callback, which completes synchronously and instantly. All subsequent async work (the actual HTTP request) runs outside that context, so child spans won't be parented correctly and the span won't be the "active" span during the request. tracer.startSpan is the correct API when the caller owns the span lifecycle.

File: packages/node/src/utils/hevy-client-observability.ts (43-59)

🧹 Maintainability - Raw string literals `"HEVY_REQUEST_ABORTED"` and `"HEVY_RETRY_EXHAUSTED"` duplicate the canonical constants without a compile-time link, so a rename in hevy-client-kubb.ts won't be caught.

Details: SAFE_OBSERVATION_CODES on lines 17-29 hardcodes the string literals "HEVY_REQUEST_ABORTED" and "HEVY_RETRY_EXHAUSTED" instead of referencing the exported constants HEVY_REQUEST_ABORTED_ERROR_CODE and HEVY_RETRY_EXHAUSTED_ERROR_CODE from @hevy-mcp/hevy-client. If those constants are ever renamed, the node-side filter will silently pass codes it should suppress, potentially leaking internal error identifiers into metric dimensions.

File: packages/node/src/utils/hevy-client-observability.ts (17-29)

🧹 Maintainability - Accessing an underscore-prefixed private property of the MCP SDK Protocol class means any SDK upgrade can silently drop all protocol-level spans without throwing an error.

Details: installSdkErrorTracking reads protocol._requestHandlers (lines 299-301), a private Map that is not part of the MCP SDK's public API. The guard if (!handlers) return silently disables the entire tools/call and server/discover instrumentation whenever the SDK restructures this internal (e.g. after an SDK upgrade). Per user instructions, the stdio observability suite must be rerun after SDK upgrades — this is precisely why: silent fallback means missing spans with no test failure.

File: packages/node/src/index.ts (299-342)

Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Review using Guidelines Learn how

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Improve OpenTelemetry trace fidelity across MCP server telemetry

✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Adds privacy-safe, opaque MCP session correlation IDs propagated across Node spans, tool
 observers, and Sentry sanitization.
• Instruments Hevy API client with naturally timed request-start/finish and retry-wait spans, richer
 outcome classification (success/retryable/terminal/expected), and safe error codes.
• Adds cache-state observability (hit/miss/refresh/expired/inflight_wait) to the shared TTL cache
 and wires it through the exercise template catalog.
• Introduces bounded lifecycle, SDK protocol, discovery, and process-exception telemetry with a
 normalized mcp.span.category taxonomy and enriched resource metadata (service instance ID, runtime
 info).
Diagram

graph TD
  A["mcp-session-observability.ts"] -->|"telemetrySessionId"| B["index.ts (lifecycle+SDK hooks)"]
  B --> C["tool-observer.ts"]
  B --> D["hevy-client-observability.ts"]
  D --> E["hevy-client-kubb.ts (API requests)"]
  B --> F["cache.ts / exercise-template-catalog.ts"]
  B --> G["telemetry.ts (tracer, exceptions)"]
  G --> H[["OTel Collector / Sentry"]]
  subgraph Legend
    direction LR
    _mod["Module"] ~~~ _ext{{External}}
  end
Loading
Files changed (26) +1235 / -114

Enhancement (14) +917 / -86
index.tsExport new cache, error-handler, and taxonomy telemetry types +17/-0

Export new cache, error-handler, and taxonomy telemetry types

• Adds public exports for CacheObserver types, McpToolFailureEvent/createMcpToolFailureEvent, and MCP_SPAN_CATEGORIES/McpSpanCategory.

packages/core/src/index.ts

server.tsThread cacheObserver option through server factory +3/-2

Thread cacheObserver option through server factory

• Adds an optional cacheObserver to CreateHevyMcpServerOptions and passes it into the exercise template catalog constructor.

packages/core/src/server.ts

cache.tsAdd privacy-safe cache observation hooks to AsyncTtlCache +71/-2

Add privacy-safe cache observation hooks to AsyncTtlCache

• Introduces CacheObserver/CacheObservation contracts and instruments hit, miss, refresh, expired, and inflight_wait states with best-effort start/finish scopes.

packages/core/src/utils/cache.ts

error-handler.tsEmit structured mcp.tool.failure event instead of raw diagnostic log +40/-2

Emit structured mcp.tool.failure event instead of raw diagnostic log

• Adds McpToolFailureEvent type and createMcpToolFailureEvent helper, replacing the previous freeform console.error call with a canonical structured event.

packages/core/src/utils/error-handler.ts

exercise-template-catalog.tsWire cache observer and metadata into exercise template catalog +16/-2

Wire cache observer and metadata into exercise template catalog

• Accepts an optional CacheObserver, tracks pages/items loaded, and supplies refresh reason and bucketed counts as observation metadata on fetch completion.

packages/core/src/utils/exercise-template-catalog.ts

tool-taxonomy.tsAdd MCP_SPAN_CATEGORIES taxonomy +12/-0

Add MCP_SPAN_CATEGORIES taxonomy

• Defines a normalized set of span categories (startup, session, protocol, discovery, tool, api, cache, process) and corresponding type.

packages/core/src/utils/tool-taxonomy.ts

hevy-client-kubb.tsAdd request-start and retry-wait observation hooks with outcome classification +174/-17

Add request-start and retry-wait observation hooks with outcome classification

• Introduces onRequestStart/onRetryWait callbacks, HevyApiOutcome classification (success/retryable_failure/terminal_failure/expected), expected-404 detection for read/list endpoints, and a safe error-code allowlist.

packages/hevy-client/src/hevy-client-kubb.ts

index.tsExport new API observation types +5/-0

Export new API observation types

• Adds public exports for HevyApiOutcome, HevyRequestObservationScope, HevyRequestStart, HevyRetryWait, and HevyRetryWaitScope.

packages/hevy-client/src/index.ts

index.tsAdd lifecycle, SDK, discovery, and process failure telemetry with span categories +272/-4

Add lifecycle, SDK, discovery, and process failure telemetry with span categories

• Introduces recordLifecycleFailure, installSdkErrorTracking (wrapping protocol.onerror, tools/call, and server/discover handlers), sdkToolNameStorage for safe tool-name correlation, and mcp.span.category attributes across startup/connect/run spans.

packages/node/src/index.ts

hevy-client-observability.tsEmit naturally timed API spans, retry-wait spans, and cache observer +159/-33

Emit naturally timed API spans, retry-wait spans, and cache observer

• Replaces post-hoc span creation with onRequestStart/finish pairing for accurate duration, adds onRetryWait span emission, and introduces createNodeCacheObserver mapping cache states to hevy.cache.* spans.

packages/node/src/utils/hevy-client-observability.ts

mcp-session-observability.tsAdd opaque per-session telemetry correlation ID +22/-1

Add opaque per-session telemetry correlation ID

• Adds telemetrySessionId to McpSessionContext (generated via randomUUID or injectable), exposes getCurrentMcpSessionId(), and supports configurable now()/generator for testing.

packages/node/src/utils/mcp-session-observability.ts

stdio-observability.tsAttach session ID and span category to stdio deserialize spans +7/-3

Attach session ID and span category to stdio deserialize spans

• Adds mcp.span.category=protocol and optional mcp.session.id attributes to the mcp.stdio.deserialize span.

packages/node/src/utils/stdio-observability.ts

telemetry.tsNormalize exception types/codes and enrich resource metadata +94/-13

Normalize exception types/codes and enrich resource metadata

• Adds allowlists for safe exception types/codes, sets exception.type/error.category on recorded exceptions, tags process spans with mcp.span.category=process, and adds service.instance.id and process runtime metadata to the OTel resource.

packages/node/src/utils/telemetry.ts

tool-observer.tsAdd span category, session ID, and structured failure events to tool spans +25/-7

Add span category, session ID, and structured failure events to tool spans

• Tags tool/discovery spans with mcp.span.category, attaches session ID, adds structured mcp.tool.failure events for both thrown and returned errors, and simplifies Sentry scope tagging.

packages/node/src/utils/tool-observer.ts

Tests (10) +314 / -28
cache.test.tsAdd tests for cache observation states +44/-0

Add tests for cache observation states

• Covers hit/miss/refresh/inflight_wait observation events and confirms cache keys are never exposed to the observer.

packages/core/src/utils/cache.test.ts

error-handler.test.tsAdd test for canonical bounded failure event +18/-0

Add test for canonical bounded failure event

• Verifies createErrorResponse emits a structured mcp.tool.failure console.error event with bounded fields.

packages/core/src/utils/error-handler.test.ts

hevy-client.test.tsAdd tests for request timing, retry-wait, and expected 404 outcomes +96/-0

Add tests for request timing, retry-wait, and expected 404 outcomes

• Covers span timing across async response parsing, retry attempt/backoff observation sequencing, and expected outcome classification for supported 404s.

packages/hevy-client/src/hevy-client.test.ts

index.test.tsUpdate test doubles for new telemetry and cache observer hooks +6/-1

Update test doubles for new telemetry and cache observer hooks

• Adds mocks for addEvent, recordTelemetryException, createNodeCacheObserver, and createSafeErrorDiagnostic to support new lifecycle/SDK instrumentation.

packages/node/src/index.test.ts

hevy-client-observability.test.tsUpdate tests for request-start span and outcome/session attributes +56/-26

Update tests for request-start span and outcome/session attributes

• Switches from startSpan to startActiveSpan mocking and verifies new mcp.span.category, mcp.transport, and outcome attributes on API spans and metrics.

packages/node/src/utils/hevy-client-observability.test.ts

mcp-session-observability.test.tsAdd tests for session ID isolation and generation +31/-0

Add tests for session ID isolation and generation

• Verifies opaque telemetry session IDs are isolated across concurrent sessions and can be injected/generated deterministically for tests.

packages/node/src/utils/mcp-session-observability.test.ts

sentry-privacy.test.tsAdd test for MCP session ID sanitization in Sentry spans +17/-0

Add test for MCP session ID sanitization in Sentry spans

• New test verifying sanitizeSentryMcpSpan strips mcp.session.id while preserving unrelated span data.

packages/node/src/utils/sentry-privacy.test.ts

stdio-observability.test.tsMock getCurrentMcpSessionId in stdio observability tests +1/-0

Mock getCurrentMcpSessionId in stdio observability tests

• Adds a mock for the new session ID accessor used by stdio deserialization spans.

packages/node/src/utils/stdio-observability.test.ts

telemetry.test.tsAdd tests for service-instance ID generation and session ID sanitization +12/-1

Add tests for service-instance ID generation and session ID sanitization

• Verifies createServiceInstanceId supports deterministic IDs for tests and that mcp.session.id is stripped from exported Sentry spans.

packages/node/src/utils/telemetry.test.ts

tool-observer.test.tsAdd test for returned-error tool span failure event +33/-0

Add test for returned-error tool span failure event

• Verifies mcp.tool.failure events and error.type attributes are set correctly when a tool returns an error result rather than throwing.

packages/node/src/utils/tool-observer.test.ts

Other (2) +4 / -0
opaque-telemetry-correlation.mdAdd changeset for opaque telemetry correlation +2/-0

Add changeset for opaque telemetry correlation

• Empty/placeholder changeset entry for the session correlation feature.

.changeset/opaque-telemetry-correlation.md

steady-failure-events.mdAdd changeset for failure event telemetry +2/-0

Add changeset for failure event telemetry

• Empty/placeholder changeset entry for the failure telemetry feature.

.changeset/steady-failure-events.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Initialize span missing mcp.session.id ✓ Resolved 📎 Requirement gap ◔ Observability
Description
mcp.session.id is read before the stdio session context is created, so the mcp.stdio.deserialize
span for the initialize message will not include session correlation. This violates the
requirement that session-scoped spans carry a stable, privacy-safe session correlation ID.
Code

packages/node/src/utils/stdio-observability.ts[R270-273]

+	const sessionId = getCurrentMcpSessionId();
return tracer.startActiveSpan(
  "mcp.stdio.deserialize",
  {
Evidence
Rule 1 requires mcp.session.id on all session-scoped spans. In
deserializeMessageWithObservability, sessionId is captured before the initialize message
triggers recordMcpSessionStart(message), so the initialize span cannot include the newly created
session correlation ID.

Privacy-safe MCP session correlation is present on all session-scoped spans (Node)
packages/node/src/utils/stdio-observability.ts[270-286]
packages/node/src/utils/stdio-observability.ts[292-301]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The stdio `mcp.stdio.deserialize` span computes `sessionId` before `recordMcpSessionStart(message)` runs (which sets `activeStdioSession`). As a result, the first session-scoped span for the `initialize` request cannot include `mcp.session.id`.
## Issue Context
Compliance requires that a single opaque `mcp.session.id` be attached to all session-scoped spans for Node, including the span that observes the `initialize` message that activates the session context.
## Fix Focus Areas
- packages/node/src/utils/stdio-observability.ts[263-305]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Empty changeset entries ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
Two newly added Changesets contain only --- delimiters and specify neither a package bump nor a
summary, which can produce misleading/no-op release metadata and may be rejected by Changesets
validation depending on CI configuration. These files should either be removed or populated with a
proper package bump stanza and human-readable summary.
Code

.changeset/opaque-telemetry-correlation.md[R1-2]

+---
+---
Evidence
The two new Changesets are empty (only ---), unlike existing Changesets in the repo which include
a package bump line and summary text.

.changeset/opaque-telemetry-correlation.md[1-2]
.changeset/steady-failure-events.md[1-2]
.changeset/quiet-telemetry-opt-out.md[1-5]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Two new Changesets were added but they contain only frontmatter delimiters (`---`) and no package bump + summary. In this repo, Changesets are expected to declare at least one package bump and include a short release note.
### Issue Context
A valid Changeset in this repo looks like:

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Network errors miscategorized ✓ Resolved 🐞 Bug ◔ Observability
Description
HevyRequestObservation.error.category supports NetworkError, but the client always emits
category: "HevyHttpError" even when wrapping network/timeout failures (e.g., ERR_NETWORK,
ETIMEDOUT). This prevents downstream Node spans/metrics from distinguishing network failures from
HTTP failures.
Code

packages/hevy-client/src/hevy-client-kubb.ts[R522-525]

+								? error.code
+								: undefined,
+						category: "HevyHttpError",
  			},
-				});
-				if (!canRetry) {
Evidence
Network failures are explicitly identified via getNetworkCode() and wrapped as a network request
failure, but the emitted observation still hardcodes error.category to HevyHttpError. Node
observability uses that category field for telemetry attributes.

packages/hevy-client/src/hevy-client-kubb.ts[355-359]
packages/hevy-client/src/hevy-client-kubb.ts[471-486]
packages/hevy-client/src/hevy-client-kubb.ts[517-525]
packages/node/src/utils/hevy-client-observability.ts[31-40]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The request observation payload always sets `error.category` to `"HevyHttpError"`, even when the underlying failure is a network/timeout error (identified via `getNetworkCode()` and/or missing HTTP status).
### Issue Context
Downstream Node instrumentation consumes `observation.error.category` to set span/metric attributes, so a hardcoded category collapses network vs HTTP failure segmentation.
### Fix Focus Areas
- packages/hevy-client/src/hevy-client-kubb.ts[471-526]
- packages/node/src/utils/hevy-client-observability.ts[31-40]
### What to change
- When `cause` is not a `HevyHttpError` (i.e., the branch that wraps it as `new HevyHttpError("...network request failed", { code: getNetworkCode(cause) })`), set `observation.error.category` to `"NetworkError"`.
- Keep `"HevyHttpError"` for true HTTP errors (where the error originated from a non-OK HTTP response / has an HTTP status).
- Add/adjust a unit test to assert that a thrown network failure produces `error.category === "NetworkError"` in `onRequestComplete`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread packages/node/src/utils/stdio-observability.ts
Comment thread .changeset/opaque-telemetry-correlation.md
Comment thread packages/hevy-client/src/hevy-client-kubb.ts

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/node/src/utils/tool-observer.ts (1)

202-218: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prompt failures are tagged under mcp.tool.name.

Line 205 sets mcp.tool.name for every invocation. Line 215 still branches on invocation.kind === "prompt" for the context key, and the fingerprint at line 221 uses mcp-prompt-failure. A prompt failure therefore reports a prompt name under a tool-name tag. Sentry search and grouping by mcp.tool.name will mix prompts and tools.

Line 202 also computes isPrompt, and line 215 recomputes the same condition inline. Reuse the local.

🐛 Proposed fix
 		Sentry.withScope((scope) => {
-			scope.setTag("mcp.tool.name", invocation.name);
+			scope.setTag(isPrompt ? "mcp.prompt.name" : "mcp.tool.name", invocation.name);
 			scope.setTag("error.type", completion.errorType ?? "UNKNOWN_ERROR");
@@
-			scope.setContext(invocation.kind === "prompt" ? "mcpPrompt" : "mcpTool", {
+			scope.setContext(isPrompt ? "mcpPrompt" : "mcpTool", {
 				context: invocation.name,
🤖 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 `@packages/node/src/utils/tool-observer.ts` around lines 202 - 218, Update the
Sentry scope in the failure-reporting flow to tag the invocation name as a
prompt-specific key when isPrompt is true, while retaining mcp.tool.name for
tools. Reuse the existing isPrompt local for the context key instead of
recomputing invocation.kind, and ensure the prompt fingerprint uses the same
prompt classification so prompt failures remain separated from tool failures.
🧹 Nitpick comments (14)
packages/node/src/index.ts (1)

231-297: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hoist the session ID lookup at lines 262-264.

getCurrentMcpSessionId() is called twice in the same expression. Both calls read the same AsyncLocalStorage store in the same tick, so the result is identical. Every other site in this PR assigns the value to a local sessionId first. Match that pattern.

♻️ Proposed change
 		} else {
+			const sessionId = getCurrentMcpSessionId();
 			tracer.startActiveSpan(
 				"mcp.sdk.failure",
 				{
 					attributes: {
 						"mcp.span.category": "protocol",
-						...(getCurrentMcpSessionId()
-							? { "mcp.session.id": getCurrentMcpSessionId() }
-							: {}),
+						...(sessionId ? { "mcp.session.id": sessionId } : {}),
 					},
 				},
🤖 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 `@packages/node/src/index.ts` around lines 231 - 297, In
installSdkErrorTracking, assign getCurrentMcpSessionId() once to a local
sessionId before tracer.startActiveSpan, then reuse that variable for the
conditional mcp.session.id attribute instead of calling the lookup twice.
packages/node/src/utils/telemetry.ts (2)

189-202: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Prefer a direct random fallback over an HMAC keyed with Math.random().

The fallback places all entropy in the HMAC key (pid plus Math.random()) and hashes a constant message (name:version). This works, but it is an unusual use of HMAC and depends on the non-cryptographic Math.random(). randomBytes comes from the same node:crypto module that the fallback already imports, so it is available on this path.

♻️ Proposed simpler fallback
-	return createHmac("sha256", `${process.pid}:${Math.random()}`)
-		.update(`${name}:${version}`)
-		.digest("hex")
-		.slice(0, 32);
+	return randomBytes(16).toString("hex");

Add randomBytes to the existing node:crypto import.

🤖 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 `@packages/node/src/utils/telemetry.ts` around lines 189 - 202, Update
createServiceInstanceId’s fallback to use randomBytes from the existing
node:crypto import, replacing the HMAC construction and its Math.random()-based
key with a direct random opaque identifier while preserving the existing
32-character return length.

128-145: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Move span.end() into a finally block.

If recordTelemetryException throws, the callback skips span.end() and the span never closes. The outer try swallows the error, so the leak stays silent. recordTelemetryException currently guards its own body, so this is defensive only.

♻️ Proposed defensive fix
 				(span) => {
-					const normalized = normalizeTelemetryError(error);
-					const code = getSafeExceptionCode(error);
-					recordTelemetryException(
-						error,
-						{
-							"exception.source": source,
-							"error.category": normalized.name,
-							...(code ? { "error.code": code } : {}),
-						},
-						span,
-					);
-					span.end();
+					try {
+						const normalized = normalizeTelemetryError(error);
+						const code = getSafeExceptionCode(error);
+						recordTelemetryException(
+							error,
+							{
+								"exception.source": source,
+								"error.category": normalized.name,
+								...(code ? { "error.code": code } : {}),
+							},
+							span,
+						);
+					} finally {
+						span.end();
+					}
 				},
🤖 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 `@packages/node/src/utils/telemetry.ts` around lines 128 - 145, Update the
callback passed to tracer.startActiveSpan around recordTelemetryException so
span.end() executes in a finally block, including when normalization, code
extraction, or exception recording throws. Preserve the existing telemetry
attributes and outer error-handling behavior.
packages/node/src/utils/tool-observer.test.ts (1)

46-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the session ID reaches the span attributes.

The mock returns "session-1", but the startActiveSpan test double at line 18 discards _options. No test in this file captures span attributes, so nothing verifies that createAttributes attaches mcp.session.id. The PR objective requires a deterministic test for session propagation onto tool spans.

Capture the options argument and assert the attribute.

💚 Proposed change
 	startActiveSpan: vi.fn(
 		(
 			_name: string,
-			_options: unknown,
+			options: unknown,
 			callback: (span: unknown) => unknown,
 		) => {
 			testDoubles.activeSpanDepth += 1;
+			testDoubles.lastSpanOptions = options;
 			return Promise.resolve(callback(testDoubles.span)).finally(() => {
 				testDoubles.activeSpanDepth -= 1;
 			});
 		},
 	),

Add lastSpanOptions: undefined as unknown to testDoubles, then assert in a new test:

it("attaches the MCP session ID to the tool span", async () => {
	const scope = startScope();
	await scope.run(() => Promise.resolve("ok"));

	expect(testDoubles.lastSpanOptions).toMatchObject({
		attributes: { "mcp.session.id": "session-1", "mcp.span.category": "tool" },
	});
});

Also applies to: 78-78

🤖 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 `@packages/node/src/utils/tool-observer.test.ts` at line 46, Update the
startActiveSpan test double and its testDoubles state to capture the span
options argument, then add a deterministic test that runs the tool scope and
asserts the captured attributes include mcp.session.id set to session-1 and
mcp.span.category set to tool. Apply the same assertion coverage to the related
occurrence.
packages/node/src/utils/telemetry.test.ts (1)

353-361: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the createServiceInstanceId fallback branches.

The test only covers the happy path with an injected generator. The new validation logic in createServiceInstanceId has three untested branches: an empty generator result, a result longer than 128 characters, and a generator that throws. The createHmac, hmacUpdate, and hmacDigest test doubles already exist in this file, so asserting the fallback is cheap.

💚 Proposed additional cases
 	it("supports deterministic service-instance IDs for tests", async () => {
 		vi.resetModules();
 		const mod = await import("./telemetry.js");
 
 		expect(mod.createServiceInstanceId(() => "instance-test-id")).toBe(
 			"instance-test-id",
 		);
 	});
+
+	it("falls back to a process-local ID for invalid or failing generators", async () => {
+		vi.resetModules();
+		const mod = await import("./telemetry.js");
+
+		expect(mod.createServiceInstanceId(() => "")).toBe("abcdef0123456789");
+		expect(mod.createServiceInstanceId(() => "x".repeat(129))).toBe(
+			"abcdef0123456789",
+		);
+		expect(
+			mod.createServiceInstanceId(() => {
+				throw new Error("no entropy");
+			}),
+		).toBe("abcdef0123456789");
+	});
🤖 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 `@packages/node/src/utils/telemetry.test.ts` around lines 353 - 361, Add tests
alongside “supports deterministic service-instance IDs for tests” covering
createServiceInstanceId when the injected generator returns an empty string,
returns a value exceeding 128 characters, and throws. For each case, assert that
the function falls back to the expected HMAC-based ID using the existing
createHmac, hmacUpdate, and hmacDigest test doubles.
packages/node/src/utils/tool-observer.ts (1)

25-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the discovery category from the taxonomy instead of a hardcoded name list.

DISCOVERY_TOOL_NAMES holds one literal tool name. Any new discovery tool must be added to this Set by hand, and nothing connects the Set to the tool registry. The invocation already carries invocation.taxonomy with feature, kind, and operation fields. Using the taxonomy keeps one source of truth for the span category.

If the taxonomy has no field that identifies discovery operations, add one in packages/core rather than maintaining the name list here.

Also applies to: 62-66

🤖 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 `@packages/node/src/utils/tool-observer.ts` at line 25, Replace the hardcoded
DISCOVERY_TOOL_NAMES check in the tool-observer classification flow with the
discovery indicator from invocation.taxonomy, preserving the existing span
category behavior for discovery operations. If the taxonomy defined in
packages/core lacks a field identifying discovery operations, add that field
there and update the tool registry to populate it, keeping taxonomy as the
single source of truth.
packages/core/src/utils/cache.test.ts (1)

143-147: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert the complete observer input.

The callback destructures state, so this test does not detect a future key
property on the observation object. Store each observation before destructuring it.
Then assert that each object contains only state.

Proposed test update
-		const events: string[] = [];
+		const events: string[] = [];
+		const observations: Array<{ state: string }> = [];
 		const cache = new AsyncTtlCache<string, string>({
 			ttlMs: 60_000,
 			maxSize: 2,
 			observer: {
-				start: ({ state }) => {
+				start: (observation) => {
+					observations.push(observation);
+					const { state } = observation;
 					events.push(`start:${state}`);
 					return { finish: () => events.push(`finish:${state}`) };
 				},
 			},
 		});

Add an exact observations assertion for miss, inflight_wait, hit, and
refresh.

🤖 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 `@packages/core/src/utils/cache.test.ts` around lines 143 - 147, Update the
observer callback in the cache test to record each complete observation object
before reading state, then assert exact observation objects for miss,
inflight_wait, hit, and refresh containing only state. Keep the existing event
assertions while ensuring any unexpected key property causes the test to fail.
packages/node/src/utils/hevy-client-observability.test.ts (2)

76-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert with SpanStatusCode.OK instead of the literal 1.

The literal couples the test to the numeric value of the enum and hides the intent. @opentelemetry/api is already a dependency of this test file's subject.

♻️ Proposed change
-		expect(testDoubles.span.setStatus).toHaveBeenCalledWith({ code: 1 });
+		expect(testDoubles.span.setStatus).toHaveBeenCalledWith({
+			code: SpanStatusCode.OK,
+		});

Add import { SpanStatusCode } from "@opentelemetry/api"; to the imports.

🤖 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 `@packages/node/src/utils/hevy-client-observability.test.ts` at line 76, Update
the status assertion in the observability test to compare against
SpanStatusCode.OK rather than the numeric literal 1, and import SpanStatusCode
from `@opentelemetry/api` alongside the existing imports.

20-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The telemetry.js mock omits startSpan, so the retry-wait and cache paths cannot be tested.

packages/node/src/utils/hevy-client-observability.ts calls tracer.startSpan at line 142 for onRetryWait and at line 164 for createNodeCacheObserver. This mock provides only startActiveSpan. Any test that exercises those paths fails with "startSpan is not a function". Both paths are new behavior in this PR and currently have no coverage here. Add startSpan to the mock and add cases for the retry-wait span attributes and the cache span metadata.

♻️ Proposed mock extension
 const testDoubles = vi.hoisted(() => ({
 	span: {
 		addEvent: vi.fn(),
 		end: vi.fn(),
 		setAttribute: vi.fn(),
 		setStatus: vi.fn(),
 	},
 	startActiveSpan: vi.fn((...args: unknown[]) => {
 		const callback = args.at(-1) as (span: unknown) => unknown;
 		return callback(testDoubles.span);
 	}),
+	startSpan: vi.fn(() => testDoubles.span),
 	apiCallsAdd: vi.fn(),
 	apiDurationRecord: vi.fn(),
 }));
 vi.mock("./telemetry.js", () => ({
-	tracer: { startActiveSpan: testDoubles.startActiveSpan },
+	tracer: {
+		startActiveSpan: testDoubles.startActiveSpan,
+		startSpan: testDoubles.startSpan,
+	},
🤖 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 `@packages/node/src/utils/hevy-client-observability.test.ts` around lines 20 -
21, Extend the telemetry mock used by the observability tests to provide
tracer.startSpan alongside startActiveSpan, then add coverage for onRetryWait
and createNodeCacheObserver that verifies their span attributes and cache
metadata. Use the existing test doubles and assertion patterns in
hevy-client-observability.test.ts.
packages/hevy-client/src/hevy-client.test.ts (2)

156-158: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

A single microtask tick may not be enough to reach the request-start callback.

await Promise.resolve() yields one microtask. The generated client and withTimeout add await points before line 401 runs emitRequestStart. If any of those points consume more than one tick, events is empty and the assertion fails. Use vi.waitFor so the test waits for the start event instead of assuming a fixed tick count.

♻️ Proposed change
 		const request = client.getUserInfo();
-		await Promise.resolve();
-		expect(events).toEqual(["start"]);
+		await vi.waitFor(() => expect(events).toEqual(["start"]));
 		releaseBody();
 		await request;
🤖 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 `@packages/hevy-client/src/hevy-client.test.ts` around lines 156 - 158, Update
the getUserInfo request-start test to use vi.waitFor around the events
assertion, waiting until the “start” event is emitted instead of relying on a
single Promise.resolve microtask. Keep the expected events value unchanged.

198-220: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The test name promises read-endpoint coverage that the body does not provide.

The title says "marks supported read and later-page 404s as expected outcomes". The body only exercises getWorkouts({ page: 2 }), which produces end_of_list. No case covers EXPECTED_READ_404_ENDPOINTS and the not_found reason. Add a read case, for example getWorkout("id") asserting { outcome: "expected", expectedReason: "not_found" }, or narrow the title to the later-page case.

🤖 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 `@packages/hevy-client/src/hevy-client.test.ts` around lines 198 - 220, The
test named “marks supported read and later-page 404s as expected outcomes” must
cover both behaviors promised by its title. Extend the test using a supported
single-read client call such as getWorkout("id") and assert an expected
observation with expectedReason "not_found", while preserving the existing
page-2 end_of_list assertion; alternatively, narrow the test name to describe
only the later-page case.
packages/node/src/utils/hevy-client-observability.ts (2)

17-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import the exported error-code constants instead of repeating the literals.

@hevy-mcp/hevy-client exports HEVY_REQUEST_ABORTED_ERROR_CODE and HEVY_RETRY_EXHAUSTED_ERROR_CODE. This set hardcodes "HEVY_REQUEST_ABORTED" and "HEVY_RETRY_EXHAUSTED". If either constant value changes, this copy silently drops the code from telemetry and no compiler error appears. The whole set also duplicates SAFE_OBSERVATION_CODES in packages/hevy-client/src/hevy-client-kubb.ts lines 138-150; consider exporting the set once from the client package and importing it here.

♻️ Minimum change
+import {
+	HEVY_REQUEST_ABORTED_ERROR_CODE,
+	HEVY_RETRY_EXHAUSTED_ERROR_CODE,
+} from "`@hevy-mcp/hevy-client`";
+
 const SAFE_OBSERVATION_CODES = new Set([
 	"EAI_AGAIN",
 	"ECONNABORTED",
 	"ECONNREFUSED",
 	"ECONNRESET",
 	"ENETUNREACH",
 	"ENOTFOUND",
 	"ERR_NETWORK",
 	"ERR_SOCKET_TIMEOUT",
 	"ETIMEDOUT",
-	"HEVY_REQUEST_ABORTED",
-	"HEVY_RETRY_EXHAUSTED",
+	HEVY_REQUEST_ABORTED_ERROR_CODE,
+	HEVY_RETRY_EXHAUSTED_ERROR_CODE,
 ]);
🤖 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 `@packages/node/src/utils/hevy-client-observability.ts` around lines 17 - 29,
Update SAFE_OBSERVATION_CODES to import and use HEVY_REQUEST_ABORTED_ERROR_CODE
and HEVY_RETRY_EXHAUSTED_ERROR_CODE from `@hevy-mcp/hevy-client` instead of
hardcoded values. Prefer reusing an exported safe-observation-code set from the
client package if available, while preserving the existing telemetry behavior.

43-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use startSpan for the API request span.

startApiSpan returns the span from the callback before the HTTP attempt runs, so the active context does not cover the request. The retry-wait span created by onRetryWait() and any auto-instrumented HTTP spans attach to the enclosing tool span instead of this API attempt. Replace tracer.startActiveSpan(..., (span) => span) with tracer.startSpan(...) unless the request work is explicitly moved into the span’s context.

♻️ Minimum clarity change
 function startApiSpan(start: HevyRequestStart) {
 	const sessionId = getCurrentMcpSessionId();
-	return tracer.startActiveSpan(
-		`hevy.api.${start.method}`,
-		{
-			attributes: {
-				"mcp.span.category": "api",
-				"http.method": start.method,
-				"hevy.api.endpoint": start.endpoint,
-				"hevy.api.retry_count_bucket": bucketCount(start.retryCount),
-				"mcp.transport": getCurrentMcpTransport(),
-				...(sessionId ? { "mcp.session.id": sessionId } : {}),
-			},
-		},
-		(span) => span,
-	);
+	return tracer.startSpan(`hevy.api.${start.method}`, {
+		attributes: {
+			"mcp.span.category": "api",
+			"http.method": start.method,
+			"hevy.api.endpoint": start.endpoint,
+			"hevy.api.retry_count_bucket": bucketCount(start.retryCount),
+			"mcp.transport": getCurrentMcpTransport(),
+			...(sessionId ? { "mcp.session.id": sessionId } : {}),
+		},
+	});
 }

Note: Update packages/node/src/utils/hevy-client-observability.test.ts lines 63-75 if this matches the existing assertion.

🤖 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 `@packages/node/src/utils/hevy-client-observability.ts` around lines 43 - 59,
Update startApiSpan to create and return the API request span with
tracer.startSpan instead of startActiveSpan with a callback, preserving the
existing span name and attributes. Adjust the corresponding test assertion in
the observability test if it currently expects the active-span callback
behavior.
packages/hevy-client/src/hevy-client-kubb.ts (1)

123-137: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider adding /v1/exercise_history/:exerciseTemplateId to the expected read 404 set.

SAFE_DYNAMIC_ENDPOINTS maps /v1/exercise_history/ to /v1/exercise_history/:exerciseTemplateId, but EXPECTED_READ_404_ENDPOINTS omits it. A 404 for an unknown exercise template therefore classifies as terminal_failure and raises a span error, while the equivalent 404 for /v1/workouts/:workoutId classifies as expected. If the omission is deliberate, no change is needed.

♻️ Proposed addition
 const EXPECTED_READ_404_ENDPOINTS = new Set([
 	"/v1/body_measurements/:date",
+	"/v1/exercise_history/:exerciseTemplateId",
 	"/v1/exercise_templates/:exerciseTemplateId",
 	"/v1/routine_folders/:folderId",
 	"/v1/routines/:routineId",
 	"/v1/workouts/:workoutId",
 ]);
🤖 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 `@packages/hevy-client/src/hevy-client-kubb.ts` around lines 123 - 137, Add
"/v1/exercise_history/:exerciseTemplateId" to EXPECTED_READ_404_ENDPOINTS so
unknown exercise-template history reads classify 404 responses as expected,
consistent with the SAFE_DYNAMIC_ENDPOINTS mapping and other read endpoints.
🤖 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 @.changeset/steady-failure-events.md:
- Around line 1-2: Update the changeset metadata by declaring source-code bumps
for `@hevy-mcp/core` and `@hevy-mcp/hevy-client`, and include hevy-mcp when those
packages are part of the public bundled release. Ensure the relevant changeset
file contains valid package entries instead of empty front matter.

In `@packages/hevy-client/src/hevy-client-kubb.ts`:
- Around line 499-526: Update the HevyRequestObservation construction in the
request error handling flow to classify retry-exhausted requests as
terminal_failure by checking the exhaustion condition before canRetry. In the
same observation error object, derive category from whether error.status is
absent so network failures are reported as NetworkError while HTTP failures
remain HevyHttpError; preserve the existing safe code handling.

In `@packages/node/src/index.ts`:
- Around line 194-229: Update markSdkToolFailure to accept an optional
error-type parameter that defaults to the safe diagnostic category, and use it
for both the mcp.tool.failure event and span error.type attribute and telemetry
fields. At the createToolError validation call site, pass VALIDATION_ERROR
explicitly; leave the tools/call catch path on the diagnostic-category default.

In `@packages/node/src/utils/hevy-client-observability.ts`:
- Around line 61-70: Update finishApiSpan to set the HTTP status only when
observation.status > 0, preventing fallback status 0 from being recorded.
Replace the deprecated http.status_code attribute with http.response.status_code
and use http.request.method for method attributes wherever this telemetry flow
defines them, retaining legacy names only when required by existing collectors.

In `@packages/node/src/utils/mcp-session-observability.test.ts`:
- Around line 51-69: Update the test case around createMcpSessionContext and
runWithMcpSessionContext to call recordMcpSessionStart within a scoped session
context before checking testDoubles.sessionStartedAdd. Keep the existing
assertion that the opaque telemetry ID is not passed to the metric, ensuring the
metric path is actually exercised.

---

Outside diff comments:
In `@packages/node/src/utils/tool-observer.ts`:
- Around line 202-218: Update the Sentry scope in the failure-reporting flow to
tag the invocation name as a prompt-specific key when isPrompt is true, while
retaining mcp.tool.name for tools. Reuse the existing isPrompt local for the
context key instead of recomputing invocation.kind, and ensure the prompt
fingerprint uses the same prompt classification so prompt failures remain
separated from tool failures.

---

Nitpick comments:
In `@packages/core/src/utils/cache.test.ts`:
- Around line 143-147: Update the observer callback in the cache test to record
each complete observation object before reading state, then assert exact
observation objects for miss, inflight_wait, hit, and refresh containing only
state. Keep the existing event assertions while ensuring any unexpected key
property causes the test to fail.

In `@packages/hevy-client/src/hevy-client-kubb.ts`:
- Around line 123-137: Add "/v1/exercise_history/:exerciseTemplateId" to
EXPECTED_READ_404_ENDPOINTS so unknown exercise-template history reads classify
404 responses as expected, consistent with the SAFE_DYNAMIC_ENDPOINTS mapping
and other read endpoints.

In `@packages/hevy-client/src/hevy-client.test.ts`:
- Around line 156-158: Update the getUserInfo request-start test to use
vi.waitFor around the events assertion, waiting until the “start” event is
emitted instead of relying on a single Promise.resolve microtask. Keep the
expected events value unchanged.
- Around line 198-220: The test named “marks supported read and later-page 404s
as expected outcomes” must cover both behaviors promised by its title. Extend
the test using a supported single-read client call such as getWorkout("id") and
assert an expected observation with expectedReason "not_found", while preserving
the existing page-2 end_of_list assertion; alternatively, narrow the test name
to describe only the later-page case.

In `@packages/node/src/index.ts`:
- Around line 231-297: In installSdkErrorTracking, assign
getCurrentMcpSessionId() once to a local sessionId before
tracer.startActiveSpan, then reuse that variable for the conditional
mcp.session.id attribute instead of calling the lookup twice.

In `@packages/node/src/utils/hevy-client-observability.test.ts`:
- Line 76: Update the status assertion in the observability test to compare
against SpanStatusCode.OK rather than the numeric literal 1, and import
SpanStatusCode from `@opentelemetry/api` alongside the existing imports.
- Around line 20-21: Extend the telemetry mock used by the observability tests
to provide tracer.startSpan alongside startActiveSpan, then add coverage for
onRetryWait and createNodeCacheObserver that verifies their span attributes and
cache metadata. Use the existing test doubles and assertion patterns in
hevy-client-observability.test.ts.

In `@packages/node/src/utils/hevy-client-observability.ts`:
- Around line 17-29: Update SAFE_OBSERVATION_CODES to import and use
HEVY_REQUEST_ABORTED_ERROR_CODE and HEVY_RETRY_EXHAUSTED_ERROR_CODE from
`@hevy-mcp/hevy-client` instead of hardcoded values. Prefer reusing an exported
safe-observation-code set from the client package if available, while preserving
the existing telemetry behavior.
- Around line 43-59: Update startApiSpan to create and return the API request
span with tracer.startSpan instead of startActiveSpan with a callback,
preserving the existing span name and attributes. Adjust the corresponding test
assertion in the observability test if it currently expects the active-span
callback behavior.

In `@packages/node/src/utils/telemetry.test.ts`:
- Around line 353-361: Add tests alongside “supports deterministic
service-instance IDs for tests” covering createServiceInstanceId when the
injected generator returns an empty string, returns a value exceeding 128
characters, and throws. For each case, assert that the function falls back to
the expected HMAC-based ID using the existing createHmac, hmacUpdate, and
hmacDigest test doubles.

In `@packages/node/src/utils/telemetry.ts`:
- Around line 189-202: Update createServiceInstanceId’s fallback to use
randomBytes from the existing node:crypto import, replacing the HMAC
construction and its Math.random()-based key with a direct random opaque
identifier while preserving the existing 32-character return length.
- Around line 128-145: Update the callback passed to tracer.startActiveSpan
around recordTelemetryException so span.end() executes in a finally block,
including when normalization, code extraction, or exception recording throws.
Preserve the existing telemetry attributes and outer error-handling behavior.

In `@packages/node/src/utils/tool-observer.test.ts`:
- Line 46: Update the startActiveSpan test double and its testDoubles state to
capture the span options argument, then add a deterministic test that runs the
tool scope and asserts the captured attributes include mcp.session.id set to
session-1 and mcp.span.category set to tool. Apply the same assertion coverage
to the related occurrence.

In `@packages/node/src/utils/tool-observer.ts`:
- Line 25: Replace the hardcoded DISCOVERY_TOOL_NAMES check in the tool-observer
classification flow with the discovery indicator from invocation.taxonomy,
preserving the existing span category behavior for discovery operations. If the
taxonomy defined in packages/core lacks a field identifying discovery
operations, add that field there and update the tool registry to populate it,
keeping taxonomy as the single source of truth.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d5b19b36-7355-441b-9bce-55629118fab2

📥 Commits

Reviewing files that changed from the base of the PR and between 8a524de and e64b465.

📒 Files selected for processing (26)
  • .changeset/opaque-telemetry-correlation.md
  • .changeset/steady-failure-events.md
  • packages/core/src/index.ts
  • packages/core/src/server.ts
  • packages/core/src/utils/cache.test.ts
  • packages/core/src/utils/cache.ts
  • packages/core/src/utils/error-handler.test.ts
  • packages/core/src/utils/error-handler.ts
  • packages/core/src/utils/exercise-template-catalog.ts
  • packages/core/src/utils/tool-taxonomy.ts
  • packages/hevy-client/src/hevy-client-kubb.ts
  • packages/hevy-client/src/hevy-client.test.ts
  • packages/hevy-client/src/index.ts
  • packages/node/src/index.test.ts
  • packages/node/src/index.ts
  • packages/node/src/utils/hevy-client-observability.test.ts
  • packages/node/src/utils/hevy-client-observability.ts
  • packages/node/src/utils/mcp-session-observability.test.ts
  • packages/node/src/utils/mcp-session-observability.ts
  • packages/node/src/utils/sentry-privacy.test.ts
  • packages/node/src/utils/stdio-observability.test.ts
  • packages/node/src/utils/stdio-observability.ts
  • packages/node/src/utils/telemetry.test.ts
  • packages/node/src/utils/telemetry.ts
  • packages/node/src/utils/tool-observer.test.ts
  • packages/node/src/utils/tool-observer.ts

Comment thread .changeset/steady-failure-events.md
Comment thread packages/hevy-client/src/hevy-client-kubb.ts
Comment thread packages/node/src/index.ts
Comment thread packages/node/src/utils/hevy-client-observability.ts
Comment thread packages/node/src/utils/mcp-session-observability.test.ts
@chrisdoc

chrisdoc commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Resolved in b670f6d: initialize spans now receive the generated session ID; changesets are valid package bumps with summaries; exhausted retries are terminal and network failures use NetworkError; SDK validation failures are labeled explicitly; HTTP span attributes use stable semantic names and omit status 0; session metric privacy test exercises the metric path; API spans use startSpan and retry/cache observer coverage was added. Focused review suites: 70 tests passed.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Cloudflare Worker preview

@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 16.47kB (7.66%) ⬆️⚠️, exceeding the configured threshold of 5%.

Bundle name Size Change
hevy-mcp-esm 231.46kB 16.47kB (7.66%) ⬆️⚠️

Affected Assets, Files, and Routes:

view changes for bundle: hevy-mcp-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
src-vfF6TBZO.mjs (New) 229.28kB 229.28kB 100.0% 🚀
src-BGQCPTet.mjs (Deleted) -212.81kB 0 bytes -100.0% 🗑️

Files in src-vfF6TBZO.mjs:

  • ./src/utils/hevy-client-observability.ts → Total Size: 4.79kB

  • ./src/utils/mcp-session-observability.ts → Total Size: 4.17kB

  • ./src/index.ts → Total Size: 14.62kB

  • ./src/utils/telemetry.ts → Total Size: 6.6kB

  • ./src/utils/tool-observer.ts → Total Size: 10.03kB

  • ./src/utils/stdio-observability.ts → Total Size: 7.92kB

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

MCP tool token cost

Measured with o200k_base over the complete json-serialized mcp tools/list result payload: { tools }.
Targets are advisory except the enforced total-token budget.

Metric Current Target Status
Tools 26 ≤ 20 Above target
Total tokens 7964 ≤ 8900 Within target
Average tokens/tool 306.31 < 600 Within target

Component totals

Component Tokens
name 153
description 560
inputSchema 3400
outputSchema 3051
annotations 606

Change from baseline

Metric Baseline Current Delta
Tools 26 26 0
Total tokens 7964 7964 0
Average tokens/tool 306.31 306.31 0

Per-tool changes

Tool Baseline Current Delta
create-body-measurement 328 328 0
create-exercise-template 354 354 0
create-routine 420 420 0
create-routine-folder 109 109 0
create-workout 532 532 0
get-body-measurement 300 300 0
get-body-measurements 372 372 0
get-exercise-history 247 247 0
get-exercise-template 172 172 0
get-exercise-templates 254 254 0
get-routine 332 332 0
get-routine-folder 142 142 0
get-routine-folders 224 224 0
get-routines 277 277 0
get-training-summary 624 624 0
get-user-info 133 133 0
get-workout 322 322 0
get-workout-count 109 109 0
get-workout-events 499 499 0
get-workouts 273 273 0
replace-workout-exercises 416 416 0
search-exercise-templates 258 258 0
search-routines 280 280 0
update-body-measurement 328 328 0
update-routine 423 423 0
update-workout 232 232 0

Component changes

Component Delta
name 0
description 0
inputSchema 0
outputSchema 0
annotations 0

Per-tool breakdown

Tool name description inputSchema outputSchema annotations Total Share of total
get-training-summary 5 30 31 531 19 624 7.84%
create-workout 5 20 470 0 31 532 6.68%
get-workout-events 6 22 84 360 19 499 6.27%
update-routine 5 18 363 0 31 423 5.31%
create-routine 5 20 358 0 31 420 5.27%
replace-workout-exercises 7 17 354 0 32 416 5.22%
get-body-measurements 7 24 62 252 19 372 4.67%
create-exercise-template 6 18 292 0 32 354 4.45%
get-routine 5 23 31 247 18 332 4.17%
create-body-measurement 7 24 259 0 32 328 4.12%
update-body-measurement 7 24 259 0 32 328 4.12%
get-workout 5 23 33 235 18 322 4.04%
get-body-measurement 7 25 43 198 19 300 3.77%
search-routines 5 24 42 182 19 280 3.52%
get-routines 5 23 62 160 19 277 3.48%
get-workouts 5 25 47 169 19 273 3.43%
search-exercise-templates 7 25 113 86 19 258 3.24%
get-exercise-templates 7 21 62 137 19 254 3.19%
get-exercise-history 6 22 58 134 19 247 3.1%
update-workout 5 17 173 0 31 232 2.91%
get-routine-folders 7 15 62 112 20 224 2.81%
get-exercise-template 6 23 33 83 19 172 2.16%
get-routine-folder 6 22 31 56 19 142 1.78%
get-user-info 5 20 14 67 19 133 1.67%
create-routine-folder 6 15 50 0 32 109 1.37%
get-workout-count 6 20 14 42 19 109 1.37%

Per-component counts are diagnostic and non-additive because keys and separators live in complete tool objects. Per-tool counts encode each complete tool object independently. The total encodes the complete { tools } envelope, so punctuation and separators mean the per-tool values need not sum exactly to the total.

@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.19011% with 131 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.56%. Comparing base (8a524de) to head (0ea209d).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/node/src/index.ts 9.80% 85 Missing and 7 partials ⚠️
packages/node/src/utils/telemetry.ts 35.71% 18 Missing ⚠️
...ckages/node/src/utils/hevy-client-observability.ts 76.08% 1 Missing and 10 partials ⚠️
packages/core/src/utils/cache.ts 85.71% 3 Missing ⚠️
packages/hevy-client/src/hevy-client-kubb.ts 94.73% 2 Missing ⚠️
packages/node/src/utils/stdio-observability.ts 60.00% 0 Missing and 2 partials ⚠️
packages/node/src/utils/tool-observer.ts 83.33% 0 Missing and 2 partials ⚠️
...ckages/node/src/utils/mcp-session-observability.ts 83.33% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #847      +/-   ##
==========================================
- Coverage   79.36%   77.56%   -1.80%     
==========================================
  Files          67       68       +1     
  Lines        3746     3981     +235     
  Branches     1068     1141      +73     
==========================================
+ Hits         2973     3088     +115     
- Misses        412      514     +102     
- Partials      361      379      +18     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@mergify

mergify Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Unit Test Results

  1 files   65 suites   7s ⏱️
672 tests 672 ✅ 0 💤 0 ❌
678 runs  678 ✅ 0 💤 0 ❌

Results for commit 0ea209d.

@chrisdoc

chrisdoc commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Superseded by PR #848, which contains the combined issue #845 and #846 implementation.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve OpenTelemetry trace fidelity and add privacy-safe MCP session correlation

1 participant