Skip to content

fix: propagate user hash to startup spans - #683

Merged
chrisdoc merged 6 commits into
mainfrom
chrisdoc/fix-otel
Jul 18, 2026
Merged

fix: propagate user hash to startup spans#683
chrisdoc merged 6 commits into
mainfrom
chrisdoc/fix-otel

Conversation

@chrisdoc

@chrisdoc chrisdoc commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • seed user.hash before startup configuration validation
  • propagate the hash to recorded spans, including tool calls and startup validation failures
  • preserve the existing HMAC derivation for historical trace correlation
  • add regression coverage and a patch changeset

Verification

  • npx vitest run src/index.test.ts src/utils/telemetry.test.ts src/utils/telemetry-wrapper.test.ts src/utils/hevy-client-observability.test.ts
  • npm run build
  • npm run check:types
  • npm run check
  • npm run test:unit
  • npm run check:changeset

Summary by CodeRabbit

  • Enhancements
    • Switched tracing and telemetry identity from raw user IDs to deterministic pseudonymous user hashes.
    • Propagated user.hash across OpenTelemetry spans (including server build/run and request/workflow spans).
    • Aligned Sentry user context and span attribution to use the same hashed identifier.
  • Tests
    • Updated telemetry/server and wrapper tests to mock and assert user.hash behavior.
    • Removed an npm tag/commit manifest integration test; kept the remaining manifest synchronization coverage.
  • Chores
    • Added a Changesets entry for a patch version bump of hevy-mcp.

✨ PR Description

Purpose: Rename user identification from user.id to user.hash semantic convention and propagate it to all spans via a custom span processor for consistent trace correlation.

Main changes:

  • Replaced setCurrentUserId/getCurrentUserId with setCurrentUserHash/getCurrentUserHash throughout codebase
  • Implemented UserHashSpanProcessor to automatically inject user.hash attribute into every started span
  • Added early user hash seeding in runServer() before config validation for startup failure trace correlation

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-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 18, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Informational

1. Global user hash leakage 🐞 Bug ≡ Correctness
Description
UserHashSpanProcessor applies a single process-global currentUserHash to every started span, so
spans can be tagged with the wrong hash if multiple createServer()/buildServer() calls with
different API keys occur in the same process. This can break trace attribution and cross-contaminate
pseudonymous user identity across server instances.
Code

src/utils/telemetry.ts[R80-87]

+let currentUserHash: string | undefined;
-// Span processor 2: OTel Collector → Honeycomb (traces) — only if token is available
+class UserHashSpanProcessor implements SpanProcessor {
+	onStart(span: Span): void {
+		if (currentUserHash) {
+			span.setAttribute("user.hash", currentUserHash);
+		}
+	}
Relevance

⭐ Low

Team previously accepted module-scoped currentUserId telemetry state; suggests non-enforcement of
per-instance isolation (PR #443/#592).

PR-#443
PR-#592

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The telemetry module defines a module-scoped currentUserHash and a span processor that reads it on
every span start. Separately, createServer() is a public factory and buildServer() sets the
global hash from its input API key, so multiple server instances will share and overwrite the same
hash source used by the processor.

src/utils/telemetry.ts[80-99]
src/utils/telemetry.ts[169-176]
src/index.ts[163-207]

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

## Issue description
A process-global `currentUserHash` is used by `UserHashSpanProcessor` to tag *all* spans. If more than one server instance (or API key) is used in the same Node process, spans from one instance may be tagged with another instance's hash.
## Issue Context
This repo exports `createServer()` for programmatic consumption; nothing enforces a single server instance per process. `buildServer()` sets the global hash based on its `apiKey`, and the span processor reads that same global value.
## Fix Focus Areas
- src/utils/telemetry.ts[80-100]
- src/index.ts[163-207]
## Suggested fix approaches (pick one)
1) **Enforce single-user-hash invariant (minimal, safest):**
- In `setCurrentUserHash(hash)`, if `currentUserHash` is already set and differs, throw (or log + refuse to change) with a clear message that multiple server instances in one process are unsupported.
- Optionally add `clearCurrentUserHash()` for controlled teardown/testing.
2) **Make user hash instance-scoped (more work, most correct):**
- Remove the global span processor.
- Pass `userHash` explicitly through server construction (e.g., into `createNodeHevyClientOptions(userHash)` and handler wrappers) so spans started by that server consistently use that hash without relying on global mutable state.
3) **Async-context scoping (advanced):**
- Replace the global with an async-context store (e.g., `AsyncLocalStorage`) and ensure every tool invocation/request enters the correct context before starting spans; have the span processor read from the async-context store.

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


2. Global user hash leakage 🐞 Bug ≡ Correctness
Description
UserHashSpanProcessor applies a single process-global currentUserHash to every started span, so
spans can be tagged with the wrong hash if multiple createServer()/buildServer() calls with
different API keys occur in the same process. This can break trace attribution and cross-contaminate
pseudonymous user identity across server instances.
Code

src/utils/telemetry.ts[R80-87]

+let currentUserHash: string | undefined;
-// Span processor 2: OTel Collector → Honeycomb (traces) — only if token is available
+class UserHashSpanProcessor implements SpanProcessor {
+	onStart(span: Span): void {
+		if (currentUserHash) {
+			span.setAttribute("user.hash", currentUserHash);
+		}
+	}
Relevance

⭐ Low

Team previously accepted module-scoped currentUserId telemetry state; suggests non-enforcement of
per-instance isolation (PR #443/#592).

PR-#443
PR-#592

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The telemetry module defines a module-scoped currentUserHash and a span processor that reads it on
every span start. Separately, createServer() is a public factory and buildServer() sets the
global hash from its input API key, so multiple server instances will share and overwrite the same
hash source used by the processor.

src/utils/telemetry.ts[80-99]
src/utils/telemetry.ts[169-176]
src/index.ts[163-207]

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

## Issue description
A process-global `currentUserHash` is used by `UserHashSpanProcessor` to tag *all* spans. If more than one server instance (or API key) is used in the same Node process, spans from one instance may be tagged with another instance's hash.
## Issue Context
This repo exports `createServer()` for programmatic consumption; nothing enforces a single server instance per process. `buildServer()` sets the global hash based on its `apiKey`, and the span processor reads that same global value.
## Fix Focus Areas
- src/utils/telemetry.ts[80-100]
- src/index.ts[163-207]
## Suggested fix approaches (pick one)
1) **Enforce single-user-hash invariant (minimal, safest):**
- In `setCurrentUserHash(hash)`, if `currentUserHash` is already set and differs, throw (or log + refuse to change) with a clear message that multiple server instances in one process are unsupported.
- Optionally add `clearCurrentUserHash()` for controlled teardown/testing.
2) **Make user hash instance-scoped (more work, most correct):**
- Remove the global span processor.
- Pass `userHash` explicitly through server construction (e.g., into `createNodeHevyClientOptions(userHash)` and handler wrappers) so spans started by that server consistently use that hash without relying on global mutable state.
3) **Async-context scoping (advanced):**
- Replace the global with an async-context store (e.g., `AsyncLocalStorage`) and ensure every tool invocation/request enters the correct context before starting spans; have the span processor read from the async-context store.

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


3. Global user hash leakage 🐞 Bug ≡ Correctness
Description
UserHashSpanProcessor applies a single process-global currentUserHash to every started span, so
spans can be tagged with the wrong hash if multiple createServer()/buildServer() calls with
different API keys occur in the same process. This can break trace attribution and cross-contaminate
pseudonymous user identity across server instances.
Code

src/utils/telemetry.ts[R80-87]

+let currentUserHash: string | undefined;

-// Span processor 2: OTel Collector → Honeycomb (traces) — only if token is available
+class UserHashSpanProcessor implements SpanProcessor {
+	onStart(span: Span): void {
+		if (currentUserHash) {
+			span.setAttribute("user.hash", currentUserHash);
+		}
+	}
Relevance

⭐ Low

Team previously accepted module-scoped currentUserId telemetry state; suggests non-enforcement of
per-instance isolation (PR #443/#592).

PR-#443
PR-#592

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The telemetry module defines a module-scoped currentUserHash and a span processor that reads it on
every span start. Separately, createServer() is a public factory and buildServer() sets the
global hash from its input API key, so multiple server instances will share and overwrite the same
hash source used by the processor.

src/utils/telemetry.ts[80-99]
src/utils/telemetry.ts[169-176]
src/index.ts[163-207]

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

## Issue description
A process-global `currentUserHash` is used by `UserHashSpanProcessor` to tag *all* spans. If more than one server instance (or API key) is used in the same Node process, spans from one instance may be tagged with another instance's hash.
## Issue Context
This repo exports `createServer()` for programmatic consumption; nothing enforces a single server instance per process. `buildServer()` sets the global hash based on its `apiKey`, and the span processor reads that same global value.
## Fix Focus Areas
- src/utils/telemetry.ts[80-100]
- src/index.ts[163-207]
## Suggested fix approaches (pick one)
1) **Enforce single-user-hash invariant (minimal, safest):**
 - In `setCurrentUserHash(hash)`, if `currentUserHash` is already set and differs, throw (or log + refuse to change) with a clear message that multiple server instances in one process are unsupported.
 - Optionally add `clearCurrentUserHash()` for controlled teardown/testing.
2) **Make user hash instance-scoped (more work, most correct):**
 - Remove the global span processor.
 - Pass `userHash` explicitly through server construction (e.g., into `createNodeHevyClientOptions(userHash)` and handler wrappers) so spans started by that server consistently use that hash without relying on global mutable state.
3) **Async-context scoping (advanced):**
 - Replace the global with an async-context store (e.g., `AsyncLocalStorage`) and ensure every tool invocation/request enters the correct context before starting spans; have the span processor read from the async-context store.

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


Grey Divider

Qodo Logo

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5cd6f891-d7bc-496b-9ead-ce84d885ac4b

📥 Commits

Reviewing files that changed from the base of the PR and between ab02db0 and 0e14abf.

📒 Files selected for processing (2)
  • .changeset/spotty-deer-throw.md
  • tests/unit/server-manifest.test.ts

📝 Walkthrough

Walkthrough

Telemetry identity handling changes from user IDs to deterministic API-key hashes. The hash is propagated through OpenTelemetry span processors, request instrumentation, server startup spans, and Sentry context, with tests and a patch Changesets entry updated. An unrelated server-manifest integration test is removed.

Changes

User hash telemetry

Layer / File(s) Summary
User hash processor and API
src/utils/telemetry.ts, src/utils/telemetry.test.ts
Adds hashed-user context state and span processing that records user.hash on started spans.
Instrumentation hash attributes
src/utils/hevy-client-observability.ts, src/utils/telemetry-wrapper.ts, src/utils/*test.ts
Replaces user.id attribution with user.hash in request and wrapper spans, updating telemetry mocks and assertions.
Server startup and build context
src/index.ts, src/index.test.ts, .changeset/user-hash-spans.md
Fingerprints API keys before server validation and startup, seeds OpenTelemetry and Sentry context, and documents the patch release.

Server manifest test cleanup

Layer / File(s) Summary
Remove npm version integration coverage
tests/unit/server-manifest.test.ts
Removes the temporary git repository test and its unused process and filesystem imports.

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

Sequence Diagram(s)

sequenceDiagram
  participant runServer
  participant Telemetry
  participant UserHashSpanProcessor
  participant OpenTelemetry
  participant Sentry
  runServer->>Telemetry: fingerprint API key
  runServer->>Telemetry: setCurrentUserHash
  runServer->>Sentry: setUser with hashed identifier
  runServer->>OpenTelemetry: start mcp.server.run span
  UserHashSpanProcessor->>OpenTelemetry: attach user.hash to started spans
Loading

Possibly related PRs

  • chrisdoc/hevy-mcp#443: Updates the centralized OpenTelemetry/Sentry processor wiring used by this telemetry change.

Suggested labels: 30 min review

Poem

I’m a rabbit with a hash in my ear,
Hopping through spans so the trail is clear.
No secret keys in my leafy pack,
Just user.hash on every track.
Telemetry blooms—hop, hop, hooray!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: propagating the user hash to startup spans.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chrisdoc/fix-otel

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.

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

qodo-free-for-open-source-projects Bot commented Jul 18, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Propagate user.hash to startup and tool spans via OTel span processor

🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Seed user.hash before config validation so startup/run spans correlate with tool spans.
• Attach user.hash to all spans via an OpenTelemetry SpanProcessor, including tool calls.
• Update tests and add a patch changeset documenting the new semantic convention usage.
Diagram

graph TD
A["src/index.ts: runServer()"] --> B["fingerprintApiKey()"] --> C["telemetry.setCurrentUserHash()"]
C --> D["UserHashSpanProcessor.onStart"] --> E["Spans include user.hash"]
A --> F["tracer.startActiveSpan: mcp.server.run"] --> E
G["Tool + HTTP wrappers"] --> H["telemetry.getCurrentUserHash()"] --> E
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use OpenTelemetry Baggage for `user.hash`
  • ➕ Designed for contextual values that should propagate across span creation and async boundaries
  • ➕ Avoids custom global state and custom span processor behavior
  • ➖ More plumbing: must set baggage early and ensure it’s present in all relevant contexts
  • ➖ Some exporters/backends may not surface baggage as span attributes without additional configuration
2. Set `user.hash` only on root spans and rely on inheritance/search
  • ➕ Less invasive: no global processor, fewer moving parts
  • ➕ Avoids mutating every span at start
  • ➖ Not all tooling/backends make it easy to query traces by a root-span-only attribute
  • ➖ Child spans (e.g., tool calls) may be harder to correlate/filter without explicit attributes

Recommendation: The PR’s approach (seed early + enforce via a SpanProcessor) is the most reliable way to guarantee user.hash is present on every span, including spans created deep in helper layers and during early startup failures. Baggage is a viable longer-term option if global state becomes problematic, but for this codebase’s current structure the processor-based approach is simpler and harder to accidentally bypass.

Files changed (10) +124 / -38

Bug fix (4) +60 / -23
index.tsSeed 'user.hash' before validation and attach it to startup/build spans +24/-8

Seed 'user.hash' before validation and attach it to startup/build spans

• Switches from 'user.id' to 'user.hash' derived via the existing HMAC fingerprint. Seeds the user hash before config validation so startup failures still correlate, and updates the run span attribute if the parsed key differs from the initial environment key.

src/index.ts

hevy-client-observability.tsAnnotate Hevy API request spans with 'user.hash' +3/-3

Annotate Hevy API request spans with 'user.hash'

• Replaces 'getCurrentUserId()' usage with 'getCurrentUserHash()' and emits the OTel semantic attribute 'user.hash' when available.

src/utils/hevy-client-observability.ts

telemetry-wrapper.tsUse 'user.hash' in tool telemetry span attributes +3/-3

Use 'user.hash' in tool telemetry span attributes

• Switches tool wrapper span enrichment from 'getCurrentUserId()'/'user.id' to 'getCurrentUserHash()'/'user.hash' so tool spans are consistently correlated.

src/utils/telemetry-wrapper.ts

telemetry.tsInject 'user.hash' into all spans via a custom SpanProcessor +30/-9

Inject 'user.hash' into all spans via a custom SpanProcessor

• Introduces 'UserHashSpanProcessor' that sets 'user.hash' on every started span based on a stored current user hash. Replaces the old user-id context helpers with 'setCurrentUserHash/getCurrentUserHash' and wires the processor ahead of the Sentry span processor.

src/utils/telemetry.ts

Tests (5) +59 / -15
index.test.tsUpdate server entry tests to assert 'user.hash' on startup spans +22/-2

Update server entry tests to assert 'user.hash' on startup spans

• Renames telemetry mocks from user ID to user hash and adds assertions that 'mcp.server.run' spans include the correct 'user.hash' for both normal and secret API key cases.

src/index.test.ts

hevy-client-observability.test.tsUpdate Hevy client observability tests to use 'user.hash' +5/-5

Update Hevy client observability tests to use 'user.hash'

• Renames mocked user context accessors and asserts outbound HTTP spans are annotated with 'user.hash' instead of 'user.id'.

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

stdio-observability.test.tsUpdate stdio observability tests to mock user-hash telemetry API +2/-2

Update stdio observability tests to mock user-hash telemetry API

• Adjusts telemetry mocks to the new 'setCurrentUserHash/getCurrentUserHash' functions to keep stdio observability tests aligned with the new context API.

src/utils/stdio-observability.test.ts

telemetry-wrapper.test.tsUpdate tool wrapper tests to expect 'user.hash' attributes +6/-6

Update tool wrapper tests to expect 'user.hash' attributes

• Renames the user context concept from ID to hash throughout tests and verifies tool invocation spans include 'user.hash' while preserving safe argument redaction behavior.

src/utils/telemetry-wrapper.test.ts

telemetry.test.tsAdd regression test ensuring user hash is added on every span start +24/-0

Add regression test ensuring user hash is added on every span start

• Enhances NodeTracerProvider mocking to capture constructor options and verifies the new span processor sets 'user.hash' on span start when a current hash is configured.

src/utils/telemetry.test.ts

Other (1) +5 / -0
user-hash-spans.mdAdd patch changeset for propagating 'user.hash' on spans +5/-0

Add patch changeset for propagating 'user.hash' on spans

• Introduces a patch changeset documenting adoption of the OTel 'user.hash' semantic convention and propagation to all recorded spans.

.changeset/user-hash-spans.md

@github-actions

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 and never fail CI.

Metric Current Target Status
Tools 25 ≤ 20 Above target
Total tokens 12667
Average tokens/tool 506.68 < 600 Within target

Change from baseline

Metric Baseline Current Delta
Tools 25 25 0
Total tokens 12667 12667 0
Average tokens/tool 506.68 506.68 0

Per-tool changes

Tool Baseline Current Delta
create-body-measurement 669 669 0
create-exercise-template 421 421 0
create-routine 564 564 0
create-routine-folder 166 166 0
create-workout 655 655 0
get-body-measurement 628 628 0
get-body-measurements 629 629 0
get-exercise-history 808 808 0
get-exercise-template 276 276 0
get-exercise-templates 308 308 0
get-routine 552 552 0
get-routine-folder 245 245 0
get-routine-folders 270 270 0
get-routines 586 586 0
get-training-summary 923 923 0
get-user-info 244 244 0
get-workout 522 522 0
get-workout-count 195 195 0
get-workout-events 670 670 0
get-workouts 540 540 0
search-exercise-templates 426 426 0
search-routines 465 465 0
update-body-measurement 667 667 0
update-routine 562 562 0
update-workout 672 672 0

Per-tool breakdown

Tool Tokens Share of total
get-training-summary 923 7.29%
get-exercise-history 808 6.38%
update-workout 672 5.31%
get-workout-events 670 5.29%
create-body-measurement 669 5.28%
update-body-measurement 667 5.27%
create-workout 655 5.17%
get-body-measurements 629 4.97%
get-body-measurement 628 4.96%
get-routines 586 4.63%
create-routine 564 4.45%
update-routine 562 4.44%
get-routine 552 4.36%
get-workouts 540 4.26%
get-workout 522 4.12%
search-routines 465 3.67%
search-exercise-templates 426 3.36%
create-exercise-template 421 3.32%
get-exercise-templates 308 2.43%
get-exercise-template 276 2.18%
get-routine-folders 270 2.13%
get-routine-folder 245 1.93%
get-user-info 244 1.93%
get-workout-count 195 1.54%
create-routine-folder 166 1.31%

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 Jul 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.43%. Comparing base (93ab7b8) to head (0e14abf).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #683      +/-   ##
==========================================
+ Coverage   95.33%   95.43%   +0.10%     
==========================================
  Files          47       47              
  Lines        2080     2083       +3     
  Branches      570      571       +1     
==========================================
+ Hits         1983     1988       +5     
+ Misses         34       32       -2     
  Partials       63       63              

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Unit Test Results

  1 files   49 suites   4s ⏱️
672 tests 672 ✅ 0 💤 0 ❌
676 runs  676 ✅ 0 💤 0 ❌

Results for commit 0e14abf.

♻️ This comment has been updated with latest results.

@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 cleanly migrates from user.id to user.hash, introduces a UserHashSpanProcessor to propagate the hash to every span automatically, and seeds the user context before startup validation. The logic is sound and well-tested. A few minor issues are noted below.

3 issues detected:

🐞 Bug - `getCurrentUserHash()` is invoked twice — once to guard the conditional and once to populate the value — instead of being stored in a variable. 🛠️

Details: getCurrentUserHash() is called twice in the ternary — once for the truthiness check and once to read the value. Although currentUserHash is module-level synchronous state and is unlikely to change between the two calls within a single event-loop tick, the pattern is fragile and produces an unnecessary second function call.

File: src/utils/hevy-client-observability.ts (24-26)

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

🧹 Maintainability - The branch on lines 251-254 that updates the span attribute and re-sets the user hash can never execute because `initialUserHash` and `userHash` are always derived from the same env var value.

Details: The userHash !== initialUserHash guard (lines 251-254) can never be true in practice. Both initialUserHash and the hash computed inside the span derive from process.env.HEVY_API_KEY. configuredApiKey reads the env var directly; parseConfig returns env.HEVY_API_KEY || "", and assertApiKey would have called process.exit(1) on an empty/missing key before reaching line 250. The two hashes will therefore always be equal when both are defined, making the conditional update branch dead code.

File: src/index.ts (250-254)

🧹 Maintainability - `user.hash` is set both by the `UserHashSpanProcessor` on every span and by manual `attributes` spreads in multiple call sites, creating duplicated and potentially diverging logic.

Details: UserHashSpanProcessor.onStart already stamps user.hash on every span automatically. The same attribute is then also explicitly set in buildServer, runServer, and withTelemetry span attribute objects. The explicit sets are redundant, making the UserHashSpanProcessor the only necessary mechanism. Keeping both paths risks the two getting out of sync if the attribute key is ever renamed or the processor is removed.

File: src/utils/telemetry.ts (82-94)

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 src/utils/hevy-client-observability.ts Outdated
Comment on lines 24 to 26
...(getCurrentUserHash()
? { "user.hash": getCurrentUserHash() }
: {}),

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.

🐞 Bug - Double State Read: Capture the return value in a local variable before the spread:

const userHash = getCurrentUserHash();
...(userHash ? { "user.hash": userHash } : {}),
Suggested change
...(getCurrentUserHash()
? { "user.hash": getCurrentUserHash() }
: {}),
...((() => { const userHash = getCurrentUserHash(); return userHash ? { "user.hash": userHash } : {}; })()),
Is this review accurate? Use 👍 or 👎 to rate it

If you want to tell us more, use /gs feedback e.g. /gs feedback this review doesn't make sense, I disagree, and it keeps repeating over and over

@mergify

mergify Bot commented Jul 18, 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

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request transitions the telemetry implementation from using user.id to the OpenTelemetry user.hash semantic convention, ensuring the user hash is propagated to every recorded span. It introduces a UserHashSpanProcessor to automatically append the user hash to spans and updates the corresponding tests and utility files. The feedback suggests also seeding and updating the Sentry user context alongside the OpenTelemetry user hash during server startup and configuration parsing to ensure consistent user tracking across both platforms.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/index.ts
Comment on lines +233 to +235
if (initialUserHash) {
setCurrentUserHash(initialUserHash);
}

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.

medium

To ensure that Sentry error events captured during startup validation failures (e.g., when validateApiKey throws an error) are also associated with the correct user context, we should seed the Sentry user ID alongside the OpenTelemetry user hash.

Suggested change
if (initialUserHash) {
setCurrentUserHash(initialUserHash);
}
if (initialUserHash) {
setCurrentUserHash(initialUserHash);
Sentry.setUser({ id: initialUserHash });
}

Comment thread src/index.ts Outdated
Comment on lines +251 to +254
if (userHash !== initialUserHash) {
setCurrentUserHash(userHash);
span.setAttribute("user.hash", userHash);
}

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.

medium

Similarly, if the parsed API key differs from the initial environment API key, we should update the Sentry user context to match the new user hash.

Suggested change
if (userHash !== initialUserHash) {
setCurrentUserHash(userHash);
span.setAttribute("user.hash", userHash);
}
if (userHash !== initialUserHash) {
setCurrentUserHash(userHash);
Sentry.setUser({ id: userHash });
span.setAttribute("user.hash", userHash);
}

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

🧹 Nitpick comments (1)
src/utils/telemetry.ts (1)

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

Explicit user.hash span attributes are redundant.

Because UserHashSpanProcessor automatically injects the user.hash attribute into every started span via its onStart hook, explicitly passing user.hash in the attributes object when starting spans is architecturally redundant.

Consider removing the manual attribute definitions to reduce duplication:

  • src/utils/telemetry.ts#L82-L94: this processor is the root mechanism that applies the hash globally.
  • src/utils/hevy-client-observability.ts#L24-L25: optionally remove this manual attribute injection.
  • src/utils/telemetry-wrapper.ts#L130-L130: optionally remove this manual attribute injection.
  • src/index.ts#L174-L174: optionally remove this manual attribute injection.
  • src/index.ts#L242-L242: optionally remove this manual attribute injection.

Note: If you choose to remove these explicit attributes, you will also need to update the corresponding unit tests (e.g., in hevy-client-observability.test.ts, telemetry-wrapper.test.ts, and index.test.ts). The tests currently mock the tracer and assert that user.hash is explicitly passed into the attributes argument, which won't be true once it's solely handled by the processor's post-creation lifecycle.

🤖 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 `@src/utils/telemetry.ts` around lines 82 - 94, The UserHashSpanProcessor
already applies user.hash globally, so remove the redundant explicit user.hash
attributes from src/utils/hevy-client-observability.ts:24-25,
src/utils/telemetry-wrapper.ts:130, and src/index.ts:174 and 242; update the
corresponding tests to stop asserting those attributes are passed directly,
while leaving UserHashSpanProcessor in src/utils/telemetry.ts:82-94 as the sole
injection mechanism.
🤖 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.

Nitpick comments:
In `@src/utils/telemetry.ts`:
- Around line 82-94: The UserHashSpanProcessor already applies user.hash
globally, so remove the redundant explicit user.hash attributes from
src/utils/hevy-client-observability.ts:24-25,
src/utils/telemetry-wrapper.ts:130, and src/index.ts:174 and 242; update the
corresponding tests to stop asserting those attributes are passed directly,
while leaving UserHashSpanProcessor in src/utils/telemetry.ts:82-94 as the sole
injection mechanism.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7cf21131-d569-4d0e-ae82-5c1bc2b27546

📥 Commits

Reviewing files that changed from the base of the PR and between d5d5213 and 90a4577.

📒 Files selected for processing (10)
  • .changeset/user-hash-spans.md
  • src/index.test.ts
  • src/index.ts
  • src/utils/hevy-client-observability.test.ts
  • src/utils/hevy-client-observability.ts
  • src/utils/stdio-observability.test.ts
  • src/utils/telemetry-wrapper.test.ts
  • src/utils/telemetry-wrapper.ts
  • src/utils/telemetry.test.ts
  • src/utils/telemetry.ts

@qodo-code-review

qodo-code-review Bot commented Jul 18, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 65 rules

Grey Divider


Informational

1. Global user hash leakage 🐞 Bug ≡ Correctness
Description
UserHashSpanProcessor applies a single process-global currentUserHash to every started span, so
spans can be tagged with the wrong hash if multiple createServer()/buildServer() calls with
different API keys occur in the same process. This can break trace attribution and cross-contaminate
pseudonymous user identity across server instances.
Code

src/utils/telemetry.ts[R80-87]

+let currentUserHash: string | undefined;

-// Span processor 2: OTel Collector → Honeycomb (traces) — only if token is available
+class UserHashSpanProcessor implements SpanProcessor {
+	onStart(span: Span): void {
+		if (currentUserHash) {
+			span.setAttribute("user.hash", currentUserHash);
+		}
+	}
Relevance

⭐ Low

Team previously accepted module-scoped currentUserId telemetry state; suggests non-enforcement of
per-instance isolation (PR #443/#592).

PR-#443
PR-#592

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The telemetry module defines a module-scoped currentUserHash and a span processor that reads it on
every span start. Separately, createServer() is a public factory and buildServer() sets the
global hash from its input API key, so multiple server instances will share and overwrite the same
hash source used by the processor.

src/utils/telemetry.ts[80-99]
src/utils/telemetry.ts[169-176]
src/index.ts[163-207]

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

## Issue description
A process-global `currentUserHash` is used by `UserHashSpanProcessor` to tag *all* spans. If more than one server instance (or API key) is used in the same Node process, spans from one instance may be tagged with another instance's hash.

## Issue Context
This repo exports `createServer()` for programmatic consumption; nothing enforces a single server instance per process. `buildServer()` sets the global hash based on its `apiKey`, and the span processor reads that same global value.

## Fix Focus Areas
- src/utils/telemetry.ts[80-100]
- src/index.ts[163-207]

## Suggested fix approaches (pick one)
1) **Enforce single-user-hash invariant (minimal, safest):**
  - In `setCurrentUserHash(hash)`, if `currentUserHash` is already set and differs, throw (or log + refuse to change) with a clear message that multiple server instances in one process are unsupported.
  - Optionally add `clearCurrentUserHash()` for controlled teardown/testing.

2) **Make user hash instance-scoped (more work, most correct):**
  - Remove the global span processor.
  - Pass `userHash` explicitly through server construction (e.g., into `createNodeHevyClientOptions(userHash)` and handler wrappers) so spans started by that server consistently use that hash without relying on global mutable state.

3) **Async-context scoping (advanced):**
  - Replace the global with an async-context store (e.g., `AsyncLocalStorage`) and ensure every tool invocation/request enters the correct context before starting spans; have the span processor read from the async-context store.

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


Grey Divider

Qodo Logo

@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 cleanly renames userIduserHash throughout the span-attribute pipeline, adds a UserHashSpanProcessor to stamp every span automatically, and seeds the hash before startup config validation. The implementation is largely correct, but the two still-open previous issues (double getCurrentUserHash() call and redundant explicit user.hash sets) remain; issue_2 (dead-code guard) appears to have been dropped in the new code and is now resolved.

2 issues detected:

🐞 Bug - `getCurrentUserHash()` is called twice — once for the truthiness check and once for the value — when a single call into a local variable would suffice. 🛠️

Details: getCurrentUserHash() is invoked twice in the ternary: once for the truthiness check and once to read the value. Although the module-level variable is synchronous, calling the getter twice is fragile and unnecessary.

File: src/utils/hevy-client-observability.ts (24-26)

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

🧹 Maintainability - `user.hash` is set both by the span processor and by explicit span attribute objects, duplicating the mechanism and introducing a future divergence risk.

Details: UserHashSpanProcessor.onStart already stamps user.hash on every span automatically (including mcp.server.build, mcp.server.run, and mcp.tool.*). The same attribute is also explicitly set in buildServer, runServer, and withTelemetry. This dual-write path means the attribute key exists in two places and the two can drift out of sync if the processor is ever removed or the key is renamed.

File: src/utils/telemetry.ts (82-94)

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 src/utils/hevy-client-observability.ts Outdated
Comment on lines 24 to 26
...(getCurrentUserHash()
? { "user.hash": getCurrentUserHash() }
: {}),

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.

🐞 Bug - Double Hash Lookup: Capture the result in a local variable and use it in both branches:

const userHash = getCurrentUserHash();
...(userHash ? { "user.hash": userHash } : {}),
Suggested change
...(getCurrentUserHash()
? { "user.hash": getCurrentUserHash() }
: {}),
...((() => { const userHash = getCurrentUserHash(); return userHash ? { "user.hash": userHash } : {}; })()),
Is this review accurate? Use 👍 or 👎 to rate it

If you want to tell us more, use /gs feedback e.g. /gs feedback this review doesn't make sense, I disagree, and it keeps repeating over and over

@chrisdoc
chrisdoc merged commit 4fb32c5 into main Jul 18, 2026
21 checks passed
@chrisdoc
chrisdoc deleted the chrisdoc/fix-otel branch July 18, 2026 09:30
@github-actions github-actions Bot mentioned this pull request Jul 18, 2026
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.

1 participant