Skip to content

feat: make OTLP metrics multi-process safe - #848

Merged
chrisdoc merged 6 commits into
mainfrom
feat/otel-metrics-clickstack
Aug 2, 2026
Merged

feat: make OTLP metrics multi-process safe#848
chrisdoc merged 6 commits into
mainfrom
feat/otel-metrics-clickstack

Conversation

@chrisdoc

@chrisdoc chrisdoc commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • share the process-scoped OpenTelemetry resource between tracer and meter providers
  • configure OTLP metrics with explicit delta temporality and add transport dimensions to API metrics
  • add metric privacy/resource assertions and portable ClickStack queries for rates, errors, retries, quantiles, and freshness
  • document the verified limitation that the pinned metrics SDK does not expose public exemplar configuration

Validation

  • npm run build
  • npm run test:unit (65 files, 674 tests)
  • npm run check
  • npm run check:types
  • focused telemetry/cache suite (6 files, 38 tests)

Closes #846

Summary by CodeRabbit

  • New Features

    • Added structured monitoring for API requests, tool calls, cache activity, retries, and server lifecycle events.
    • Added session-aware telemetry with safeguards to keep session identifiers and sensitive error details out of metrics.
    • Improved classification of API outcomes, including expected missing-page responses and end-of-list results.
    • Added configurable cache observation support for server integrations.
    • Added process-safe metrics and expanded operational monitoring views.
  • Documentation

    • Added a ClickStack metrics guide covering queries, latency, errors, freshness, and privacy considerations.
  • Bug Fixes

    • Expected 404 responses no longer trigger unnecessary retries.
    • Failure reporting now uses bounded, privacy-safe event details.

✨ PR Description

Purpose: Enable multi-process safe OTLP metrics collection and add structured observability for cache operations, API requests, and lifecycle failures.

Main changes:

  • Added delta temporality preference to OTLP metric exporter and process-level service instance ID for multi-process aggregation
  • Implemented cache observer pattern with state tracking (hit/miss/expired/inflight_wait) and observation metadata for telemetry
  • Enhanced API request observability with start/finish scopes, retry wait tracking, and outcome classification (success/retryable/terminal/expected)
  • Added lifecycle failure events for config, build, connect, and discovery phases with structured error categorization

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

Warning

Review limit reached

@chrisdoc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c707d2a-eb1c-407e-b17c-0782bdaa98ab

📥 Commits

Reviewing files that changed from the base of the PR and between 14e5792 and 3d77375.

📒 Files selected for processing (3)
  • packages/hevy-client/src/hevy-client-kubb.ts
  • packages/hevy-client/src/hevy-client.test.ts
  • packages/node/src/index.test.ts
📝 Walkthrough

Walkthrough

The PR adds structured telemetry across cache, Hevy API, MCP tools, sessions, and server lifecycle paths. It adds privacy-safe metrics, stable service-instance identity, delta temporality, cache and request observations, structured failures, tests, ClickStack queries, and Changesets metadata.

Changes

Telemetry observability

Layer / File(s) Summary
Core telemetry contracts and cache observations
packages/core/src/utils/*, packages/core/src/server.ts, packages/core/src/index.ts
Adds cache observer contracts, cache metadata, structured MCP failure events, span taxonomy, and server wiring.
Hevy request outcomes and retry spans
packages/hevy-client/src/*, packages/node/src/utils/hevy-client-observability.*
Adds request lifecycle scopes, retry-wait spans, outcome classification, expected 404 handling, sanitized error metadata, and related tests.
Node resources, sessions, and transport telemetry
packages/node/src/utils/telemetry.*, packages/node/src/utils/mcp-session-observability.*, packages/node/src/utils/stdio-observability.*
Adds service-instance resources, delta temporality, sanitized exceptions, opaque session IDs, session-aware spans, and cache observer integration.
Node lifecycle, tool, and failure telemetry
packages/node/src/index.*, packages/node/src/utils/tool-observer.*
Adds lifecycle failure reporting, SDK error handling, tool and discovery spans, standardized failure events, privacy filtering, and tests.
Metrics documentation and release metadata
docs/clickstack-metrics.md, .changeset/*
Documents ClickStack queries, metric privacy rules, freshness checks, and SDK exemplar limits. Adds patch-release metadata.

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

Possibly related issues

  • chrisdoc/hevy-mcp#846 — Covers the multi-process metrics, service identity, delta temporality, privacy, ClickStack, and validation changes.
  • chrisdoc/hevy-mcp#845 — Covers the related session, request, cache, failure, and telemetry instrumentation.

Possibly related PRs

Suggested labels: 30 min review

Poem

A rabbit watched each cache event,
And traced each request as it went.
Safe failures found their proper name,
Session IDs stayed out of the frame.
ClickStack queries joined the view,
While fresh metrics sprang anew.

🚥 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 describes the primary change: making OTLP metrics safe for multi-process use.
Linked Issues check ✅ Passed The changes address the requirements in issue #846, including resource identity, delta temporality, privacy, observability, ClickStack queries, tests, and changesets.
Out of Scope Changes check ✅ Passed The telemetry, cache, request, retry, session, and failure instrumentation changes support the linked issue objectives and are not unrelated.
✨ 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-metrics-clickstack

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 adds rich observability instrumentation (cache observation, request lifecycle scopes, retry wait spans, SDK error tracking) and multi-process telemetry safety. The core logic is well-structured with best-effort error isolation throughout. Three issues worth addressing before merge.

3 issues detected:

🐞 Bug - A connect-phase error is recorded on both the inner connect span and the outer run span, producing duplicate telemetry events. 🛠️

Details: When server.connect(transport) throws, recordLifecycleFailure is called twice for the same error: once inside the inner mcp.server.connect span (line 532) with phase "connect", and again in the outer mcp.server.run catch block (line 557) with phase "connect" (because connectAttempted is true). This emits a duplicate mcp.lifecycle.failure event and calls recordTelemetryException twice on two different spans for the same underlying error.

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

🛠️ A suggested code correction is included in the review comments.

🧹 Maintainability - Patching a private `_requestHandlers` map bypasses the SDK's public API and will silently stop working if the SDK changes its internal structure.

Details: installSdkErrorTracking accesses (protocol as unknown as SdkProtocolInternals)._requestHandlers — a private, underscore-prefixed internal field of the MCP TypeScript SDK — to intercept and wrap the tools/call and server/discover handlers. This is the same category of SDK-internal dependency that the user instructions already call out as requiring special care ("rerun the stdio observability test suite because it depends on SDK stdio internals"). If the SDK renames, removes, or restructures _requestHandlers, this code will silently stop instrumenting tool calls without any compile-time or runtime error, making the failure invisible until tracing gaps are noticed in production.

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

🎯 Scope - New public exports in published packages are treated as internal-only changes, bypassing semver signalling for downstream consumers.

Details: All three changesets (multi-process-metrics.md, opaque-telemetry-correlation.md, steady-failure-events.md) contain only the front-matter delimiters ---\n--- with no package bumps. The PR exports new public types from @hevy-mcp/core (CacheObserver, CacheObservationScope, McpSpanCategory, McpToolFailureEvent, createMcpToolFailureEvent, etc.) and from @hevy-mcp/hevy-client (HevyApiOutcome, HevyRequestObservationScope, HevyRequestStart, HevyRetryWait, HevyRetryWaitScope). Per project conventions, user-facing runtime-visible changes to published packages require versioned changesets, not empty ones.

File: .changeset/multi-process-metrics.md (1-2)

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

Comment thread packages/node/src/index.ts Outdated
@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 (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Empty changesets fail CI ✓ Resolved 🐞 Bug ≡ Correctness
Description
The three newly added .changeset/*.md files contain only --- delimiters, so they declare no
packages and will fail the repo’s check:changeset/package-changeset coverage checks for this PR’s
workspace package changes.
Code

.changeset/multi-process-metrics.md[R1-2]

+---
+---
Evidence
All three new changeset files have empty frontmatter (no package entries). The repo’s changeset
validation (check:changeset) includes a script that parses changeset frontmatter to ensure every
changed workspace package is named; with empty frontmatter, coverage is empty and the script throws.

.changeset/multi-process-metrics.md[1-2]
.changeset/opaque-telemetry-correlation.md[1-2]
.changeset/steady-failure-events.md[1-2]
package.json[61-78]
scripts/check-package-changesets.mjs[101-125]
scripts/check-package-changesets.mjs[127-139]

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

## Issue description
Three newly-added changeset files are empty (`---` / `---`) and therefore declare **no packages**. This repo enforces package coverage via `npm run check:changeset` (including `scripts/check-package-changesets.mjs`), so CI is expected to fail for this PR because it changes workspace packages but provides no changeset frontmatter naming them.
## Issue Context
- The changeset validator parses YAML frontmatter and extracts package names; empty frontmatter yields no package coverage.
- This PR changes `@hevy-mcp/core`, `@hevy-mcp/hevy-client`, and `hevy-mcp` (packages/node).
- If you include `@hevy-mcp/core` or `@hevy-mcp/hevy-client` in a changeset, the script also requires `hevy-mcp` to be included.
## Fix
Update (or replace) the added changeset files so that at least one changeset has valid frontmatter including the impacted packages, e.g.:

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



Remediation recommended

2. Brittle SDK internals hook 🐞 Bug ☼ Reliability
Description
installSdkErrorTracking assumes the private protocol._requestHandlers field is a Map and calls
.get()/.set() after only a truthiness check; an MCP SDK version that changes/removes this
internal shape would turn optional instrumentation into a startup-time throw.
Code

packages/node/src/index.ts[R303-306]

+	const toolHandler = handlers.get("tools/call");
+	if (toolHandler) {
+		handlers.set("tools/call", (request, extra) => {
+			const sessionId = getCurrentMcpSessionId();
Evidence
The code casts protocol to a local interface with _requestHandlers?: Map and only checks for
truthiness before calling handlers.get()/handlers.set(). There is no runtime shape validation,
so a non-Map truthy value would cause a handlers.get is not a function-style crash during server
construction.

packages/node/src/index.ts[299-307]
packages/node/src/index.ts[344-346]

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

## Issue description
`installSdkErrorTracking` reaches into MCP SDK internals via `(protocol as unknown as SdkProtocolInternals)._requestHandlers` and then calls `handlers.get()` / `handlers.set()` after only checking `if (!handlers) return;`. If `_requestHandlers` is present but not a `Map` (or lacks `get/set`), this will throw during server build, making telemetry instrumentation break server startup.
## Issue Context
This code is intentionally hooking private/undocumented internals for richer telemetry, so it should **fail open** (skip instrumentation) rather than crash when the SDK’s internal structure differs.
## Fix
Add runtime guards before calling `.get/.set`, e.g.:
- verify `handlers` is a `Map` (`handlers instanceof Map`) **or** has callable `get` and `set` functions
- wrap the instrumentation patching block in `try/catch` and return early on failure
Example:

ⓘ 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 .changeset/multi-process-metrics.md
Comment thread packages/node/src/index.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Make OTLP metrics multi-process safe and improve trace/error fidelity

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Share one process-scoped OpenTelemetry resource (with a stable service.instance.id) between the
 tracer and meter providers so multi-process metric aggregation stays correct.
• Configure the OTLP metric exporter with explicit delta temporality and add transport/outcome
 dimensions to API, cache, and tool metrics.
• Add opaque per-session correlation IDs, richer span categorization, cache/API request observers,
 and bounded MCP tool failure events for better trace fidelity.
• Add ClickStack SQL query docs (rates, errors, retries, quantiles, freshness) and document the
 SDK's lack of public exemplar configuration.
• Extend unit test coverage across cache, hevy-client, telemetry, and session observability modules.
Diagram

graph TD
  Resource["Shared OTel Resource"] --> Tracer["Tracer Provider"] --> TraceExporter["OTLP Trace Exporter"]
  Resource --> Meter["Meter Provider"] --> MetricExporter["OTLP Metric Exporter (delta)"]
  HevyClient["Hevy API Client"] -->|"request/retry observations"| NodeObservability["Node Observability Adapters"]
  Cache["AsyncTtlCache"] -->|"cache observations"| NodeObservability
  SessionCtx["MCP Session Context"] -->|"opaque session id"| NodeObservability
  NodeObservability --> Meter
  NodeObservability --> Tracer
  MetricExporter --> ClickHouse[("ClickStack / ClickHouse")]

  subgraph Legend
    direction LR
    _svc([Service/Provider]) ~~~ _db[(Database/Storage)] ~~~ _proc["Process/Module"]
  end
Loading
Files changed (28) +1481 / -116

Enhancement (14) +924 / -87
index.tsExport new cache observation and error-handler types +17/-0

Export new cache observation and error-handler types

• Adds public exports for CacheObserver-related types, createMcpToolFailureEvent, and MCP span category taxonomy from core.

packages/core/src/index.ts

server.tsWire optional cacheObserver into createHevyMcpServer +3/-2

Wire optional cacheObserver into createHevyMcpServer

• Adds a cacheObserver option to CreateHevyMcpServerOptions and passes it into createExerciseTemplateCatalog.

packages/core/src/server.ts

cache.tsAdd CacheObserver hooks to AsyncTtlCache +71/-2

Add CacheObserver hooks to AsyncTtlCache

• Introduces CacheObservationState/CacheObserver types and instruments AsyncTtlCache to emit best-effort start/finish observations for hit, miss, refresh, expired, and inflight-wait states, with metadata support.

packages/core/src/utils/cache.ts

error-handler.tsEmit structured McpToolFailureEvent instead of raw log +40/-2

Emit structured McpToolFailureEvent instead of raw log

• Adds McpToolFailureEvent type and createMcpToolFailureEvent helper, replacing the ad hoc console.error call with a canonical, bounded failure event.

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

exercise-template-catalog.tsFeed cache observer with refresh reason and count metadata +16/-2

Feed cache observer with refresh reason and count metadata

• Threads an optional CacheObserver into the catalog cache and attaches refresh reason, page count bucket, and item count bucket observation metadata to each fetch.

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

tool-taxonomy.tsAdd MCP span category taxonomy +12/-0

Add MCP span category taxonomy

• Introduces MCP_SPAN_CATEGORIES constant and McpSpanCategory type for classifying spans (startup, session, protocol, discovery, tool, api, cache, process).

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

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

Add request-start/retry-wait observability and outcome classification

• Adds HevyApiOutcome, HevyRequestStart/HevyRequestObservationScope, and HevyRetryWait/Scope types; classifies expected 404s (not_found/end_of_list), restricts leaked error codes to a safe allowlist, and emits start/finish observation hooks around requests and retry waits.

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

index.tsExport new client observability types +5/-0

Export new client observability types

• Re-exports HevyApiOutcome, HevyRequestObservationScope, HevyRequestStart, HevyRetryWait, and HevyRetryWaitScope from the package entrypoint.

packages/hevy-client/src/index.ts

index.tsAdd lifecycle failure recording and SDK-level error tracking +272/-4

Add lifecycle failure recording and SDK-level error tracking

• Wires cacheObserver into server creation, adds recordLifecycleFailure for config/build/connect/run phases, installs SDK-level error tracking that instruments tools/call and server/discover request handlers with spans and bounded failure events, and tags spans with mcp.span.category.

packages/node/src/index.ts

hevy-client-observability.tsRework API observability with request spans, outcomes, and cache observer +161/-33

Rework API observability with request spans, outcomes, and cache observer

• Replaces single-shot span creation with startActiveSpan wrapping the full request lifecycle, adds outcome/expected_reason/transport dimensions to metrics, adds onRetryWait span emission, and introduces createNodeCacheObserver mapping cache states to 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

• Introduces a process-local telemetrySessionId on McpSessionContext (injectable or randomUUID-generated) and exposes getCurrentMcpSessionId() for span correlation without exposing protocol-derived identifiers.

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

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

Attach session ID and span category to stdio parse spans

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

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

telemetry.tsShare resource across providers, add instance ID, and delta temporality +99/-14

Share resource across providers, add instance ID, and delta temporality

• Adds createServiceInstanceId (with safe fallback), attaches service.instance.id and process.runtime attributes to a single shared Resource used by both the tracer and meter providers, configures OTLPMetricExporter with AggregationTemporalityPreference.DELTA, restricts exception type/code leakage to safe allowlists, and tags process exception spans with mcp.span.category.

packages/node/src/utils/telemetry.ts

tool-observer.tsEnrich tool spans with category, session ID, and returned-error events +25/-7

Enrich tool spans with category, session ID, and returned-error events

• Adds mcp.span.category classification (discovery vs tool), attaches session ID to span attributes, includes tool name/error type on failure events, and records a bounded mcp.tool.failure event for MCP-returned (not thrown) errors.

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

Tests (10) +371 / -29
cache.test.tsTest cache observation lifecycle without key leakage +44/-0

Test cache observation lifecycle without key leakage

• Adds a test verifying cache hit/miss/refresh/inflight-wait observation events fire correctly and never expose cache keys.

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

error-handler.test.tsTest canonical mcp.tool.failure event shape +18/-0

Test canonical mcp.tool.failure event shape

• Adds a test asserting createErrorResponse logs the new structured mcp.tool.failure event with expected fields.

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

hevy-client.test.tsTest request timing, retry observability, and expected 404s +96/-0

Test request timing, retry observability, and expected 404s

• Adds tests covering onRequestStart/onRequestComplete timing across async body parsing, per-attempt retry observation, and correct classification of expected 404 outcomes.

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

index.test.tsUpdate server bootstrap tests for new telemetry hooks +6/-1

Update server bootstrap tests for new telemetry hooks

• Adds mocks for recordTelemetryException, createNodeCacheObserver, span.addEvent, and createSafeErrorDiagnostic to match new lifecycle instrumentation.

packages/node/src/index.test.ts

hevy-client-observability.test.tsUpdate tests for span-per-request API observability +58/-26

Update tests for span-per-request API observability

• Rewrites tests to use startActiveSpan-based onRequestStart/onRequestComplete flow, verifying outcome, transport, and category attributes are recorded.

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

mcp-session-observability.test.tsTest opaque session ID isolation and generation +56/-0

Test opaque session ID isolation and generation

• Adds tests verifying per-session opaque telemetry IDs are isolated across concurrent sessions, never leak into metric attributes, and can be injected or generated.

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

sentry-privacy.test.tsAdd test for Sentry MCP span sanitization +17/-0

Add test for Sentry MCP span sanitization

• 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 getCurrentMcpSessionId export used by stdio deserialization spans.

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

telemetry.test.tsTest shared resource and stable service.instance.id across providers +35/-2

Test shared resource and stable service.instance.id across providers

• Adds tests confirming the tracer and meter providers share the same Resource instance, that service.instance.id is included and stable, and that delta temporality is configured on the OTLP metric exporter.

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

tool-observer.test.tsTest session ID exclusion from metrics and returned-error span events +40/-0

Test session ID exclusion from metrics and returned-error span events

• Adds assertions that session IDs never leak into tool metric attributes and that MCP tool errors returned (not thrown) are recorded as span failure events.

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

Documentation (4) +186 / -0
multi-process-metrics.mdAdd changeset for multi-process metrics fix +2/-0

Add changeset for multi-process metrics fix

• Placeholder changeset entry (front-matter only) recording the multi-process metrics change.

.changeset/multi-process-metrics.md

opaque-telemetry-correlation.mdAdd changeset for opaque telemetry correlation IDs +2/-0

Add changeset for opaque telemetry correlation IDs

• Placeholder changeset entry documenting the opaque session correlation ID feature.

.changeset/opaque-telemetry-correlation.md

steady-failure-events.mdAdd changeset for canonical tool failure events +2/-0

Add changeset for canonical tool failure events

• Placeholder changeset entry documenting the bounded MCP tool failure event change.

.changeset/steady-failure-events.md

clickstack-metrics.mdAdd ClickStack metric query documentation +180/-0

Add ClickStack metric query documentation

• New doc with portable ClickHouse/ClickStack SQL queries for server/session rates, tool and API outcomes, error ratios, retries, histogram-based latency quantiles, and freshness checks; documents that the pinned metrics SDK lacks public exemplar configuration.

docs/clickstack-metrics.md

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

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/index.ts (1)

496-569: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the failure-phase label for post-connect errors in runStdioServer.

connectAttempted is set to true right after the transport is created, before server.connect even runs, and it stays true afterward. If scheduleUpdateCheck or installGracefulShutdown throws after a successful server.connect, the outer catch still records mcp.failure.phase: "connect". This mislabels a post-connect setup failure as a connection failure.

Track connect success separately from "connect attempted" so the phase label reflects where the failure actually happened.

🐛 Proposed fix to track connect success separately
 	let connectAttempted = false;
+	let connectSucceeded = false;
 
 	await tracer.startActiveSpan(
 		"mcp.server.run",
 		{
 			attributes: {
 				"mcp.span.category": "startup",
 				"mcp.transport": "stdio",
 			},
 		},
 		async (span) => {
 			try {
 				const cfg = parseConfig(process.env);
 				const apiKey = cfg.apiKey;
 				assertApiKey(apiKey);
 
 				const server = await createNodeMcpServer({ apiKey });
 				console.error("Starting MCP server in stdio mode");
 				const transport = createInstrumentedStdioTransport(
 					new StdioServerTransport(),
 				);
 				connectAttempted = true;
 
 				await tracer.startActiveSpan(
 					"mcp.server.connect",
 					{
 						attributes: {
 							"mcp.span.category": "session",
 							"mcp.transport": "stdio",
 						},
 					},
 					async (connectSpan) => {
 						try {
 							await server.connect(transport);
+							connectSucceeded = true;
 							connectSpan.setStatus({ code: SpanStatusCode.OK });
 						} catch (e) {
 							recordLifecycleFailure(connectSpan, e, "connect");
 							connectSpan.setStatus({ code: SpanStatusCode.ERROR });
 							throw e;
 						} finally {
 							connectSpan.end();
 						}
 					},
 				);
 				...
 				span.setStatus({ code: SpanStatusCode.OK });
 			} catch (e) {
-				recordLifecycleFailure(span, e, connectAttempted ? "connect" : "run");
+				recordLifecycleFailure(
+					span,
+					e,
+					connectSucceeded ? "run" : connectAttempted ? "connect" : "run",
+				);
 				recordMcpSessionTermination(
 					connectAttempted ? "connect_failure" : "startup_failure",
 				);
🤖 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 496 - 569, Update runStdioServer to
track connection success separately from connectAttempted: set the new success
indicator only after server.connect completes successfully, then use it in the
outer catch to label failures as "connect" only when connection failed and "run"
for post-connect setup errors. Preserve connectAttempted for distinguishing
startup failures from connection failures in session termination.
🧹 Nitpick comments (4)
packages/hevy-client/src/hevy-client-kubb.ts (2)

487-529: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

error.category is always "HevyHttpError", so "NetworkError" is never emitted.

Line 524 sets the category to a constant. Network failures reach this branch as a synthesized HevyHttpError with no status and a network code such as ECONNRESET. The "NetworkError" member of the HevyRequestObservation["error"]["category"] union at Line 73 is therefore dead, and consumers cannot separate transport failures from HTTP failures by category.

Derive the category from whether the original cause was an HTTP response.

♻️ Proposed change
 				const error = isHevyHttpError(cause)
 					? cause
 					: new HevyHttpError(
 							normalized.signal?.aborted
 								? "Hevy API request was canceled"
 								: "Hevy API network request failed",
 							{
 								method,
 								endpoint,
 								code: normalized.signal?.aborted
 									? HEVY_REQUEST_ABORTED_ERROR_CODE
 									: getNetworkCode(cause),
 								cause,
 							},
 						);
+				const errorCategory =
+					error.status === undefined ? "NetworkError" : "HevyHttpError";
@@
 					error: {
 						status: error.status,
 						code:
 							typeof error.code === "string" &&
 							SAFE_OBSERVATION_CODES.has(error.code)
 								? error.code
 								: undefined,
-						category: "HevyHttpError",
+						category: errorCategory,
 					},
🤖 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 487 - 529, Update
the observation construction in the request error-handling flow to derive
error.category from whether the original error includes an HTTP response: use
"HevyHttpError" for response-backed failures and "NetworkError" for transport
failures such as ECONNRESET without a status. Keep the existing status, code,
and retry behavior unchanged.

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

The safe error-code allowlist is defined twice. Both packages maintain their own copy of the same eleven-entry set. The producer already filters observation.error.code through its copy, so the consumer copy is redundant defense whose only practical effect is drift: if one list gains a code and the other does not, that code is silently dropped from telemetry. The node copy also restates "HEVY_REQUEST_ABORTED" and "HEVY_RETRY_EXHAUSTED" as string literals, even though @hevy-mcp/hevy-client exports HEVY_REQUEST_ABORTED_ERROR_CODE and HEVY_RETRY_EXHAUSTED_ERROR_CODE.

  • packages/hevy-client/src/hevy-client-kubb.ts#L138-L150: export the set (for example as SAFE_OBSERVATION_CODES) and re-export it from packages/hevy-client/src/index.ts so it becomes the single source of truth.
  • packages/node/src/utils/hevy-client-observability.ts#L17-L29: delete the local set and import the exported one from @hevy-mcp/hevy-client; this also removes the two hard-coded code literals.
🤖 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 138 - 150, Export
SAFE_OBSERVATION_CODES from hevy-client-kubb.ts and re-export it through
packages/hevy-client/src/index.ts as the single source of truth. In
packages/node/src/utils/hevy-client-observability.ts, remove the local allowlist
and import the exported set from `@hevy-mcp/hevy-client`, eliminating the
duplicated hard-coded error codes.
packages/core/src/utils/error-handler.ts (1)

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

Consider serializing the failure event before writing it to stderr.

console.error receives an object. Node formats it with util.inspect, not JSON. Log collectors that parse stderr lines cannot read the event as structured data. debugLog in packages/node/src/utils/debug.ts writes JSON.stringify output for the same reason.

If you want the event to stay machine-readable, serialize it here.

Note that the assertion in packages/core/src/utils/error-handler.test.ts at Line 39 asserts the object form. Update it if you change the call.

♻️ Proposed change
-	console.error(
-		createMcpToolFailureEvent(context ?? "unknown", policy.type, diagnostic),
-	);
+	console.error(
+		JSON.stringify(
+			createMcpToolFailureEvent(context ?? "unknown", policy.type, diagnostic),
+		),
+	);
🤖 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/error-handler.ts` around lines 114 - 116, Serialize
the failure event returned by createMcpToolFailureEvent before passing it to
console.error, matching the JSON output behavior of debugLog. Update the related
assertion in the error-handler test to expect the serialized string rather than
the object form.
packages/node/src/utils/telemetry.ts (1)

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

Consider keeping the stack trace when you record an exception.

normalizeTelemetryError returns only { name }. recordTelemetryException passes this object to recordException, so the recorded exception event has no message and no stack. Dropping message is reasonable, since messages often embed request data. Dropping stack removes debugging value. A Node.js stack trace usually contains only function names and file paths, not user data.

Keep stack on the normalized error when the input is an Error instance. This keeps the privacy control on message while restoring stack-based debugging in the collector.

♻️ Proposed change to preserve stack traces
-function normalizeTelemetryError(error: unknown): { name: string } {
+function normalizeTelemetryError(error: unknown): { name: string; stack?: string } {
 	const candidate =
 		error instanceof Error && typeof error.name === "string"
 			? error.name
 			: undefined;
 	const name =
 		candidate && SAFE_EXCEPTION_TYPES.has(candidate)
 			? candidate
 			: "UnknownError";
-	return { name };
+	return {
+		name,
+		stack: error instanceof Error ? error.stack : undefined,
+	};
 }

Also applies to: 105-123

🤖 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 84 - 94, Update
normalizeTelemetryError to include the input Error instance’s stack alongside
the sanitized name, while continuing to omit message and use “UnknownError” for
untrusted or non-Error names. Ensure recordTelemetryException passes this
normalized stack through recordException without exposing request data.
🤖 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 `@docs/clickstack-metrics.md`:
- Around line 104-107: Update the histogram explanation in both referenced
sections to clarify that the N+1th +Inf bucket must not be treated as the last
ExplicitBounds value; describe it as capped or indeterminate, and note that
bounds should cover expected latency ranges to avoid understated p95/p99
estimates.

In `@packages/node/src/utils/hevy-client-observability.ts`:
- Around line 43-59: Update startApiSpan and the onRequestStart/onRetryWait flow
so the API span remains available as the parent for request processing and later
retry-wait spans; do not return a span created by the short-lived `(span) =>
span` callback as if its context remained active. Either retain and propagate an
explicit API-span scope through the request lifecycle or track the API span and
use it when creating later spans such as onRetryWait, while preserving cleanup
when processing completes.

---

Outside diff comments:
In `@packages/node/src/index.ts`:
- Around line 496-569: Update runStdioServer to track connection success
separately from connectAttempted: set the new success indicator only after
server.connect completes successfully, then use it in the outer catch to label
failures as "connect" only when connection failed and "run" for post-connect
setup errors. Preserve connectAttempted for distinguishing startup failures from
connection failures in session termination.

---

Nitpick comments:
In `@packages/core/src/utils/error-handler.ts`:
- Around line 114-116: Serialize the failure event returned by
createMcpToolFailureEvent before passing it to console.error, matching the JSON
output behavior of debugLog. Update the related assertion in the error-handler
test to expect the serialized string rather than the object form.

In `@packages/hevy-client/src/hevy-client-kubb.ts`:
- Around line 487-529: Update the observation construction in the request
error-handling flow to derive error.category from whether the original error
includes an HTTP response: use "HevyHttpError" for response-backed failures and
"NetworkError" for transport failures such as ECONNRESET without a status. Keep
the existing status, code, and retry behavior unchanged.
- Around line 138-150: Export SAFE_OBSERVATION_CODES from hevy-client-kubb.ts
and re-export it through packages/hevy-client/src/index.ts as the single source
of truth. In packages/node/src/utils/hevy-client-observability.ts, remove the
local allowlist and import the exported set from `@hevy-mcp/hevy-client`,
eliminating the duplicated hard-coded error codes.

In `@packages/node/src/utils/telemetry.ts`:
- Around line 84-94: Update normalizeTelemetryError to include the input Error
instance’s stack alongside the sanitized name, while continuing to omit message
and use “UnknownError” for untrusted or non-Error names. Ensure
recordTelemetryException passes this normalized stack through recordException
without exposing request data.
🪄 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: 4f94eb7f-1ca7-46e7-8b2c-aef245be18e1

📥 Commits

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

📒 Files selected for processing (28)
  • .changeset/multi-process-metrics.md
  • .changeset/opaque-telemetry-correlation.md
  • .changeset/steady-failure-events.md
  • docs/clickstack-metrics.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 docs/clickstack-metrics.md Outdated
Comment thread packages/node/src/utils/hevy-client-observability.ts
@chrisdoc
chrisdoc force-pushed the feat/otel-metrics-clickstack branch from a1207a1 to 9598541 Compare August 2, 2026 15:14
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Cloudflare Worker preview

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

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Unit Test Results

  1 files   65 suites   7s ⏱️
683 tests 683 ✅ 0 💤 0 ❌
690 runs  690 ✅ 0 💤 0 ❌

Results for commit 3d77375.

♻️ This comment has been updated with latest results.

@chrisdoc

chrisdoc commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Addressed the review findings in be486be: fail-open guard for the SDK handler map; separate connect success from connect attempted and avoid duplicate connect failure events; valid package changesets; corrected +Inf histogram documentation and capped quantile naming; API request/retry work now runs under the API span context; shared safe observation-code export; NetworkError classification; serialized MCP failure stderr events; and preserved sanitized exception stacks. Validation: npm run build, npm run check, npm run check:types, npm run check:changeset, and npm run test:unit (65 files, 681 tests).

@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bundle Report

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

Bundle name Size Change
hevy-mcp-esm 232.17kB 17.19kB (8.0%) ⬆️⚠️

Affected Assets, Files, and Routes:

view changes for bundle: hevy-mcp-esm

Assets Changed:

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

Files in src-DXLS9ZG2.mjs:

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

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

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

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

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

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

@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

@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.90411% with 47 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.88%. Comparing base (8a524de) to head (3d77375).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/node/src/index.ts 77.14% 0 Missing and 24 partials ⚠️
...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 95.23% 2 Missing and 1 partial ⚠️
packages/node/src/utils/stdio-observability.ts 60.00% 0 Missing and 2 partials ⚠️
packages/node/src/utils/tool-observer.ts 81.81% 0 Missing and 2 partials ⚠️
...ckages/node/src/utils/mcp-session-observability.ts 83.33% 0 Missing and 1 partial ⚠️
packages/node/src/utils/telemetry.ts 96.55% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #848      +/-   ##
==========================================
+ Coverage   79.36%   80.88%   +1.51%     
==========================================
  Files          67       68       +1     
  Lines        3746     3996     +250     
  Branches     1068     1144      +76     
==========================================
+ Hits         2973     3232     +259     
+ Misses        412      376      -36     
- Partials      361      388      +27     

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

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

🧹 Nitpick comments (2)
packages/hevy-client/src/hevy-client.test.ts (1)

203-225: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert that an expected 404 does not retry.

The change at packages/hevy-client/src/hevy-client-kubb.ts line 560 makes expected 404s terminate immediately. This test does not check the fetch call count, so a regression that retries expected 404s would still pass. Add an assertion on fetchMock.

💚 Proposed assertion
 		expect(observations).toEqual([
 			{ outcome: "expected", expectedReason: "end_of_list" },
 		]);
+		expect(fetchMock).toHaveBeenCalledTimes(1);
 	});
🤖 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 203 - 225, Add an
assertion in the test case around client.getWorkouts to verify fetchMock is
called exactly once after the expected 404. Preserve the existing rejection and
observations assertions while ensuring expected 404 responses do not trigger
retries.
packages/hevy-client/src/hevy-client-kubb.ts (1)

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

Consider preserving the underlying network code on retry exhaustion.

Line 520 replaces error.code with HEVY_RETRY_EXHAUSTED_ERROR_CODE. The observation then reports the retry code instead of the transport code, for example ETIMEDOUT. The test at packages/hevy-client/src/hevy-client.test.ts lines 257-287 confirms this loss. If you want transport-level diagnosis in metrics, capture the original code before you overwrite it and report it in the observation.

🤖 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 `@packages/hevy-client/src/hevy-client-kubb.ts`:
- Around line 282-292: Update runRequestObservation to track whether operation
has started before invoking scope.run, marking it when execution begins. In the
synchronous catch, call operation as a fallback only if it has not started;
otherwise rethrow the original error to prevent duplicate POST or PUT requests.

In `@packages/node/src/index.test.ts`:
- Around line 303-312: Strengthen the assertion in the malformed getUserInfo
rejection test around createNodeMcpServer so it verifies the actual sanitized
console.error diagnostic, rather than merely finding one call without
"not-a-status". Assert the expected output for a response with no HTTP status,
ensuring the diagnostic omits any status value while preserving the existing
server-resolution behavior.

In `@packages/node/src/utils/hevy-client-observability.ts`:
- Around line 82-92: Update the request lifecycle around observationScope and
scope.run in hevy-client-kubb.ts so finishRequestObservation runs from the
finally path for every request outcome, including caller cancellation, timeouts,
and failures thrown by scope.run. Ensure aborted or timed-out requests pass a
valid error/failure observation, while preserving the existing
successful-response and caught-error observation behavior.

---

Nitpick comments:
In `@packages/hevy-client/src/hevy-client.test.ts`:
- Around line 203-225: Add an assertion in the test case around
client.getWorkouts to verify fetchMock is called exactly once after the expected
404. Preserve the existing rejection and observations assertions while ensuring
expected 404 responses do not trigger retries.
🪄 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: 2e4c3944-ed30-4e1f-ad3f-345c703c8bd6

📥 Commits

Reviewing files that changed from the base of the PR and between a1207a1 and 14e5792.

📒 Files selected for processing (28)
  • .changeset/multi-process-metrics.md
  • .changeset/opaque-telemetry-correlation.md
  • .changeset/steady-failure-events.md
  • docs/clickstack-metrics.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
🚧 Files skipped from review as they are similar to previous changes (16)
  • packages/core/src/utils/error-handler.test.ts
  • packages/core/src/utils/error-handler.ts
  • packages/node/src/utils/sentry-privacy.test.ts
  • packages/core/src/utils/cache.test.ts
  • .changeset/multi-process-metrics.md
  • packages/core/src/utils/tool-taxonomy.ts
  • packages/hevy-client/src/index.ts
  • packages/node/src/utils/mcp-session-observability.test.ts
  • packages/core/src/server.ts
  • packages/core/src/utils/exercise-template-catalog.ts
  • packages/node/src/utils/tool-observer.test.ts
  • packages/node/src/utils/mcp-session-observability.ts
  • packages/node/src/utils/tool-observer.ts
  • packages/core/src/utils/cache.ts
  • packages/node/src/utils/stdio-observability.ts
  • packages/node/src/utils/telemetry.ts

Comment thread packages/hevy-client/src/hevy-client-kubb.ts
Comment thread packages/node/src/index.test.ts
Comment on lines +82 to +92
onRequestStart(start) {
const span = startApiSpan(start);
return {
run<T>(operation: () => Promise<T>) {
return context.with(trace.setSpan(context.active(), span), operation);
},
finish(observation) {
finishApiSpan(span, observation);
},
};
},

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how the Hevy client uses onRequestStart scopes.
rg -n -C 10 'onRequestStart|\.finish\(|\.run\(' packages/hevy-client/src/hevy-client-kubb.ts

Repository: chrisdoc/hevy-mcp

Length of output: 3591


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant section of hevy-client-kubb.ts =="
sed -n '400,490p' packages/hevy-client/src/hevy-client-kubb.ts

echo
echo "== full finishRequestObservation and runRequestObservation definitions/usages =="
rg -n -C 4 'finishRequestObservation|runRequestObservation|observationScope|controller|AbortController|abort|fetch\(' packages/hevy-client/src/hevy-client-kubb.ts

echo
echo "== observability create/usage =="
sed -n '1,120p' packages/node/src/utils/hevy-client-observability.ts

Repository: chrisdoc/hevy-mcp

Length of output: 10932


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("packages/hevy-client/src/hevy-client-kubb.ts")
text = p.read_text()

start = text.find("while (true) {")
finish = text.find("}) as KubbClient;", start)
block = text[start:finish]

print("finishRequestObservation occurrences:", text.count("finishRequestObservation(observationScope"))
print("observationScope references in request loop:", [i+1 for i,b in enumerate(block) if b=='o' and text[i:i len("observationScope")]== observationScope")
print("finally close:", "finally {" in block, "finishRequestObservation(observationScope, observation);" in block, block.endswith("}) as KubbClient;"))

# Lightweight lexical count around the body to show all non-observationScope calls cannot close the span.
print("timeout.clear references:", ["clearTimeout(timeout)" in block, block.count("clearTimeout(timeout)")])
PY

Repository: chrisdoc/hevy-mcp

Length of output: 485


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("packages/hevy-client/src/hevy-client-kubb.ts")
text = p.read_text()

block = text[text.find("while (true) {"):text.find("}) as KubbClient;", text.find("while (true) {"))]

print("finishRequestObservation(scope, observation) occurrences:", block.count("finishRequestObservation(observationScope, observation)"))
print("clear timeout statements:", block.count("clearTimeout(timeout)"))
print("finally present:", "finally {" in block)
print("scope abort hook present:", "scope" in block and ".abort" in block, block.count(".abort"))

# Extract request body and show whether any abort handler calls finishRequestObservation or observationScope.
lines = block.splitlines()
for i, line in enumerate(lines, start=text[:block].count("\n") + 1):
    if "addEventListener" in line or "abort" in line.lower():
        print(f"line {i}: {line.strip()}")
PY

Repository: chrisdoc/hevy-mcp

Length of output: 451


Call scope?.finish(observation) for aborted or timed-out requests.

packages/hevy-client/src/hevy-client-kubb.ts only calls finishRequestObservation(observationScope, observation) on successful responses and caught request failures. When AbortController.abort() is triggered by caller cancellation or timeout, or scope.run() itself throws, the scope is discarded and the OpenTelemetry span never ends. Move finishRequestObservation into the finally block, but pass a valid error/failure observation for aborts/timeouts so spans are closed in every request path.

🤖 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 82 - 92,
Update the request lifecycle around observationScope and scope.run in
hevy-client-kubb.ts so finishRequestObservation runs from the finally path for
every request outcome, including caller cancellation, timeouts, and failures
thrown by scope.run. Ensure aborted or timed-out requests pass a valid
error/failure observation, while preserving the existing successful-response and
caught-error observation behavior.

@chrisdoc
chrisdoc merged commit 11d55d2 into main Aug 2, 2026
22 checks passed
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.

Make OTLP metrics multi-process-safe and visible in ClickStack Improve OpenTelemetry trace fidelity and add privacy-safe MCP session correlation

1 participant