Skip to content

feat: emit activity-only user metrics #917 - #918

Closed
chrisdoc wants to merge 5 commits into
mainfrom
fix/activity-only-active-users
Closed

feat: emit activity-only user metrics #917#918
chrisdoc wants to merge 5 commits into
mainfrom
fix/activity-only-active-users

Conversation

@chrisdoc

@chrisdoc chrisdoc commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Primary changes

  • Adds mcp.user.activity with bounded tool, resource, and prompt activity kinds
  • Attaches the existing pseudonymous user_hash only to this dedicated active-user metric
  • Observes resource reads through the existing privacy-safe observer contract
  • Keeps DAU, WAU, and MAU semantics consistent
  • Documents the VictoriaMetrics query template and privacy constraints

Reviewer walkthrough

  • Extends the core observation contract to include resource activity and wires resource registrations through the existing privacy-safe observer.
  • Emits the dedicated user-activity counter from the Node observer while keeping the existing all-span panel separate from product-usage panels.
  • Dashboard source and build registration: chrisdoc/grafana#27

Correctness and invariants

  • Activity kinds are bounded to tool, resource, and prompt; lifecycle, discovery/list, API child, cache, and error-only spans do not count.
  • DAU, WAU, and MAU use unique user_hash values over rolling 1d, 7d, and 30d windows, with user_hash attached only to the dedicated active-user metric.
  • Existing request-volume and outcome metrics remain unchanged.

Testing and QA

  • npm run check:types
  • Focused tests: 14 passed
  • Full unit suite: 848 passed
  • git diff --check

✨ PR Description

Purpose: Add user activity metrics tracking across tools, resources, and prompts to calculate DAU/WAU/MAU while preserving privacy through pseudonymous user hashing.

Main changes:

  • Extended ToolInvocationObservation to support "resource" and "prompt" kinds alongside "tool" to enable unified activity tracking
  • Wrapped Hevy resource handlers with observation scope capturing success/error outcomes and telemetry including content counts
  • Created activityInvocations counter emitting user_hash and activity_kind exclusively for unique user calculations

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

Resolves #917

@assert-app

assert-app Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review on Assert →

3 clusters identified

Merge candidate is ready!

@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 6, 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: 5 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

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: a10aa8e8-e17a-4a01-b4fd-af735b686c40

📥 Commits

Reviewing files that changed from the base of the PR and between d2d3398 and e4f78d7.

📒 Files selected for processing (2)
  • .changeset/activity-only-metrics.md
  • packages/node/src/utils/tool-observer.ts
📝 Walkthrough

Walkthrough

The change adds activity-only telemetry for tool, resource, and prompt calls. It instruments Hevy resources, records pseudonymous user activity, expands observation types, and documents DAU, WAU, and MAU dashboards with a separate all-span diagnostic metric.

Changes

MCP activity telemetry

Layer / File(s) Summary
Activity signal and operation classification
packages/core/src/observation.ts, packages/node/src/utils/metrics.ts, packages/node/src/utils/telemetry.ts, packages/node/src/utils/tool-observer.ts, packages/node/src/utils/tool-observer.test.ts
The observer supports tool, resource, and prompt operations. It records mcp.user.activity with the telemetry user hash and validates tool activity metrics.
Hevy resource observation
packages/core/src/resources/hevy.ts, packages/core/src/resources/hevy.test.ts, packages/core/src/server.ts
All four Hevy resources run through optional observation scopes. Successful results record duration and content counts. Handler errors remain propagated.
Activity metric documentation
docs/telemetry-data-dictionary.md, docs/telemetry-dashboards.md
The documentation defines activity kinds, user-hash restrictions, activity-only DAU/WAU/MAU queries, supported windows, and the all-span diagnostic panel.

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

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant ToolObserver
  participant Telemetry
  participant ActivityMetric
  MCPClient->>ToolObserver: invoke tool, resource, or prompt
  ToolObserver->>Telemetry: read telemetry user hash
  Telemetry-->>ToolObserver: user hash
  ToolObserver->>ActivityMetric: record activity kind and user hash
  ToolObserver-->>MCPClient: complete observed operation
Loading

Possibly related PRs

Suggested labels: 30 min review, review-pending

Poem

A rabbit watched the tools take flight,
Then counted prompts by day and night.
Resources joined the active trail,
User hashes kept each measure pale.
Four Hevy paths now leave a trace—
With ears held high, we hop in place.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The implementation meets the activity metric requirements, but dashboard acceptance criteria are assigned to an unprovided linked Grafana change. Provide the linked Grafana diff or verification showing the required DAU, WAU, MAU panels and separate all-span diagnostic panel.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The code, tests, telemetry changes, and documentation all support activity-only user metrics and resource observation.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: emitting activity-only user metrics.
✨ 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 fix/activity-only-active-users

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.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Cloudflare Worker preview

@github-actions

github-actions Bot commented Aug 6, 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.

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

Copy link
Copy Markdown

PR Summary by Qodo

Emit activity-only user metrics for tools, resources, and prompts

✨ Enhancement 📝 Documentation 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add mcp.user.activity counter emitting user_hash only for bounded user activity.
• Observe resource reads via existing observer contract to keep DAU/WAU/MAU semantics consistent.
• Document PromQL/VictoriaMetrics query template and privacy constraints for active-user panels.
Diagram

graph TD
  A["MCP server"] --> B["ToolObserver"] --> C[("Telemetry context")]
  B --> D["mcp.user.activity counter"] --> E["VictoriaMetrics/Grafana"]
  A --> F["Tools/Prompts"] --> B
  A --> G["Resources"] --> B

  subgraph Legend
    direction LR
    _svc([Service/Module]) ~~~ _db[(State/Context)]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Derive active users from tracing spans only
  • ➕ No new metrics needed; reuse existing span attributes
  • ➕ Avoids emitting any metric with user_hash
  • ➖ Harder to compute distinct users efficiently in metrics backends
  • ➖ Span coverage/sampling can skew DAU/WAU/MAU and mixes diagnostic vs product usage semantics
2. Backend-side distinct counting (e.g., HLL sketches) without user_hash labels
  • ➕ Avoids per-user label cardinality and any user_hash exposure in metrics
  • ➕ Purpose-built for unique counting at scale
  • ➖ Requires backend support and more infrastructure complexity
  • ➖ Harder to validate and explain; may not be available in current stack
3. Session-based active user metric
  • ➕ Lower event volume and fewer instrumentation points
  • ➕ Avoids subtle per-primitive semantics
  • ➖ Not aligned with product usage intent (sessions != meaningful activity)
  • ➖ May over/under-count depending on connection lifecycles and retries

Recommendation: Keep the PR’s approach: a dedicated activity counter with tightly bounded activity_kind and user_hash emitted only for product-activity events. This cleanly separates diagnostic tracing/session signals from product usage measurement, while making the DAU/WAU/MAU query explicit and enforceable via documentation and label-scoping constraints.

Files changed (10) +172 / -21

Enhancement (6) +100 / -19
observation.tsExtend observation kind union to include resources +1/-1

Extend observation kind union to include resources

• Updates 'ToolInvocationObservation.kind' to allow 'resource' in addition to 'tool' and 'prompt', enabling resource handlers to participate in the same privacy-safe observation contract.

packages/core/src/observation.ts

hevy.tsWrap resource reads with observation scope and outcome/duration reporting +61/-9

Wrap resource reads with observation scope and outcome/duration reporting

• Adds 'withResourceObservation' to wrap resource handlers with best-effort observer start/finish, measuring duration and recording result buckets. Extends 'registerHevyResources' to accept an optional observer and applies the wrapper to all resource registrations.

packages/core/src/resources/hevy.ts

server.tsWire server observer into resource registration +1/-1

Wire server observer into resource registration

• Passes 'options.observer' into 'registerHevyResources' so resource reads are included in the same activity observation pipeline as tools and prompts.

packages/core/src/server.ts

metrics.tsAdd mcp.user.activity counter for bounded user activity +5/-0

Add mcp.user.activity counter for bounded user activity

• Defines a new OpenTelemetry counter 'mcp.user.activity' intended for user activity across tool/resource/prompt calls, separate from existing tool invocation and outcome metrics.

packages/node/src/utils/metrics.ts

telemetry.tsExpose current telemetry user hash getter +4/-0

Expose current telemetry user hash getter

• Adds 'getTelemetryUserHash()' to allow metrics instrumentation to read the current pseudonymous user hash without broadening its use across unrelated metrics.

packages/node/src/utils/telemetry.ts

tool-observer.tsEmit activity-only metric and support resource span/attribute naming +28/-8

Emit activity-only metric and support resource span/attribute naming

• Adds best-effort emission of 'mcp.user.activity' only when 'user_hash' is present, labeled by bounded 'activity_kind'. Generalizes attribute and span naming to support 'tool', 'resource', and 'prompt' kinds (e.g., 'mcp.resource.<name>' and 'mcp.resource.name').

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

Tests (2) +34 / -0
hevy.test.tsAdd test asserting resource handlers are observed as activity +27/-0

Add test asserting resource handlers are observed as activity

• Introduces a focused test that registers Hevy resources with an observer and asserts 'observer.start' is called with 'kind: "resource"' and that 'finish' records a success outcome.

packages/core/src/resources/hevy.test.ts

tool-observer.test.tsVerify activity counter emits kind and user_hash for tool calls +7/-0

Verify activity counter emits kind and user_hash for tool calls

• Updates mocks to include 'activityInvocations' and 'getTelemetryUserHash', then asserts the observer increments the new activity metric with 'activity_kind: "tool"' and the expected 'user_hash'.

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

Documentation (2) +38 / -2
telemetry-dashboards.mdDocument active-user panels and PromQL template for mcp.user.activity +31/-0

Document active-user panels and PromQL template for mcp.user.activity

• Adds an 'Active users' section defining activity-only semantics and which spans do not count. Provides a VictoriaMetrics/PromQL query template for DAU/WAU/MAU and guidance to keep the prior all-span panel as diagnostic only.

docs/telemetry-dashboards.md

telemetry-data-dictionary.mdDefine activity_kind values and tighten user_hash privacy rules +7/-2

Define activity_kind values and tighten user_hash privacy rules

• Adds 'activity kind' as a bounded dimension ('tool|resource|prompt'). Updates privacy guidance to allow user_hash only on the dedicated activity counter for aggregated DAU/WAU/MAU, with explicit prohibitions on per-user views.

docs/telemetry-data-dictionary.md

@charliecreates charliecreates Bot changed the title feat: emit activity-only user metrics feat: emit activity-only user metrics #917 Aug 6, 2026

@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 wires mcp.user.activity through tool, resource, and prompt primitives and the withResourceObservation wrapper is well-structured. One functional correctness issue stands out in the new resource observation layer, and a leftover dead variable was introduced in the span-attribute helper.

2 issues detected:

🐞 Bug - `readResource` converts all thrown errors into a returned value, making the `catch` block in `withResourceObservation` unreachable and causing every failure to be counted as a successful invocation.

Details: readResource catches every thrown error internally and returns a ReadResourceResult error payload — it never re-throws. Because of this, the catch (error) block in withResourceObservation is dead code for all four registered resources. Any underlying API failure will be observed with outcome: "success" and the hardcoded isError: false, silently hiding failures in the mcp.tool.outcomes and activity metrics.

File: packages/core/src/resources/hevy.ts (68-88)

🧹 Maintainability - `isPrompt` is assigned but never read; it is leftover from the refactor that introduced the three-way `kind` conditional. 🛠️

Details: isPrompt is declared at line 70 but is never referenced anywhere in createAttributes. The three-way inline conditional at lines 77-81 replaced the old isPrompt usage directly, leaving the assignment as unreferenced dead code that may produce a lint warning and misleads future readers.

File: packages/node/src/utils/tool-observer.ts (69-70)

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

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 on lines +69 to +70
const kind = invocation.kind ?? "tool";
const isPrompt = kind === "prompt";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Maintainability - Dead Variable: Remove the const isPrompt = kind === "prompt"; line entirely.

Suggested change
const kind = invocation.kind ?? "tool";
const isPrompt = kind === "prompt";
const kind = invocation.kind ?? "tool";
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

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Unit Test Results

  1 files   75 suites   22s ⏱️
841 tests 841 ✅ 0 💤 0 ❌
848 runs  848 ✅ 0 💤 0 ❌

Results for commit e4f78d7.

♻️ This comment has been updated with latest results.

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

🤖 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/telemetry-dashboards.md`:
- Around line 17-21: Update the DAU question in the telemetry dashboard table to
describe the 1d query window as the rolling “last 24 hours” rather than “today”;
keep the existing source and query window unchanged.

In `@docs/telemetry-data-dictionary.md`:
- Around line 74-79: The telemetry documentation must consistently define
user_hash handling and retention. Update the metric privacy statement to exempt
only mcp.user.activity, then revise the activity-counter retention text to
explicitly specify 30-day retention and link to the exact 30-day trace/user-hash
policy section in telemetry-dashboards.md; retain 90-day retention for other
aggregate metrics.

In `@packages/core/src/resources/hevy.ts`:
- Around line 52-90: Update withResourceObservation and readResource so
retrieval failures remain identifiable after conversion to
createResourceErrorResult: return or propagate a discriminated error result,
then have withResourceObservation finish with outcome "thrown_error" and isError
true for that path while preserving the MCP error response. Add coverage
verifying a rejected resource retrieval produces the error response and error
telemetry.

In `@packages/node/src/utils/metrics.ts`:
- Around line 16-19: Add and stage a non-empty Changeset entry for the package
containing packages/node/src/utils/metrics.ts, documenting the runtime-visible
telemetry change introduced by activityInvocations. Use the repository’s
existing Changeset format and select the appropriate release impact for that
package.

In `@packages/node/src/utils/tool-observer.ts`:
- Around line 69-82: The resource error paths currently use the tool name
attribute instead of the resource-specific attribute. In the observer
implementation, reuse one kind-specific name-attribute mapping for the main span
attributes and all failure/exception handling paths around the resource error
sites, including the logic near lines 318, 354, and 368; add a test verifying
resource failures record mcp.resource.name rather than mcp.tool.name.
- Around line 256-264: Update the observation flow around
recordMcpToolInvocation to compute activityKind before that call and identify
resource operations. Skip all legacy tool metrics—invocation, outcome, error,
and duration—for resource activity, while continuing to emit mcp.user.activity
for every activity kind.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a598abcb-6019-468a-b8fc-b85e9a942de8

📥 Commits

Reviewing files that changed from the base of the PR and between d632c55 and d2d3398.

📒 Files selected for processing (10)
  • docs/telemetry-dashboards.md
  • docs/telemetry-data-dictionary.md
  • packages/core/src/observation.ts
  • packages/core/src/resources/hevy.test.ts
  • packages/core/src/resources/hevy.ts
  • packages/core/src/server.ts
  • packages/node/src/utils/metrics.ts
  • packages/node/src/utils/telemetry.ts
  • packages/node/src/utils/tool-observer.test.ts
  • packages/node/src/utils/tool-observer.ts

Comment on lines +17 to +21
| Panel | Source | Query window | Question answered |
| -------------------------- | ------------------- | ------------ | --------------------------------------------------------------- |
| Daily active users (DAU) | `mcp.user.activity` | `1d` | How many unique users called a tool, resource, or prompt today? |
| Weekly active users (WAU) | `mcp.user.activity` | `7d` | How many unique users were active in the last week? |
| Monthly active users (MAU) | `mcp.user.activity` | `30d` | How many unique users were active in the last month? |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Describe the 1d window as rolling.

The query uses [1d], which measures the previous 24 hours. The table wording “today” implies a calendar-day value. Replace it with “last 24 hours” or document a calendar-day query.

🤖 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 `@docs/telemetry-dashboards.md` around lines 17 - 21, Update the DAU question
in the telemetry dashboard table to describe the 1d query window as the rolling
“last 24 hours” rather than “today”; keep the existing source and query window
unchanged.

Comment on lines +74 to +79
The pseudonymous user hash is also emitted on the dedicated user-activity
counter solely to calculate unique DAU, WAU, and MAU. It is not emitted on
request-volume, outcome, duration, API, or session metrics. Activity panels must
aggregate by user hash and must not expose per-user behavior histories or saved
per-user views. The activity counter is subject to the 30-day trace/user-hash
retention policy below.

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Define one retention rule for user_hash metrics.

Line 58 says that metrics never contain a user hash, but this change adds user_hash to mcp.user.activity. Update that statement to exempt only the dedicated activity metric.

Line 78 references a 30-day policy “below”, but this file has no retention section. docs/telemetry-dashboards.md Lines 96-102 specify 90-day retention for aggregate metrics and 30-day retention only for traces containing user_hash. State explicitly that activity metric series containing user_hash use 30-day retention, and link the exact policy section.

🤖 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 `@docs/telemetry-data-dictionary.md` around lines 74 - 79, The telemetry
documentation must consistently define user_hash handling and retention. Update
the metric privacy statement to exempt only mcp.user.activity, then revise the
activity-counter retention text to explicitly specify 30-day retention and link
to the exact 30-day trace/user-hash policy section in telemetry-dashboards.md;
retain 90-day retention for other aggregate metrics.

Comment on lines +52 to +90
function withResourceObservation(
name: string,
observer: ToolObserver | undefined,
handler: (uri: URL, context: ServerContext) => Promise<ReadResourceResult>,
): (uri: URL, context: ServerContext) => Promise<ReadResourceResult> {
return async (uri, context) => {
const startedAt = Date.now();
let scope;
try {
scope = memoizeObservationScope(
observer?.start({ name, kind: "resource" }),
);
} catch {
scope = undefined;
}

try {
const result = await (scope
? scope.run(() => handler(uri, context))
: handler(uri, context));
void scope?.finish({
outcome: "success",
durationMs: Date.now() - startedAt,
result: {
isError: false,
hasStructuredContent: false,
contentCountBucket: bucketCount(result.contents.length),
},
});
return result;
} catch (error) {
void scope?.finish({
outcome: "thrown_error",
durationMs: Date.now() - startedAt,
});
throw error;
}
};
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Record handled resource failures as failures.

readResource catches retrieval errors and returns createResourceErrorResult at Lines 45-49. Line 72 then records that returned error result as outcome: "success" and Line 76 hard-codes isError: false. API failures therefore increment successful outcomes and do not enter error telemetry.

Move resource-error conversion into this wrapper, or return a discriminated result from readResource. Preserve the MCP error response, but finish the observation with an error outcome. Add coverage for a rejected resource retrieval.

🤖 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/resources/hevy.ts` around lines 52 - 90, Update
withResourceObservation and readResource so retrieval failures remain
identifiable after conversion to createResourceErrorResult: return or propagate
a discriminated error result, then have withResourceObservation finish with
outcome "thrown_error" and isError true for that path while preserving the MCP
error response. Add coverage verifying a rejected resource retrieval produces
the error response and error telemetry.

Comment on lines +16 to +19
/** User activity across tool, resource, and prompt calls. */
export const activityInvocations = meter.createCounter("mcp.user.activity", {
description: "MCP user activity by primitive kind",
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

changesets="$(git diff --name-only origin/main...HEAD -- .changeset)"
if [ -z "$changesets" ]; then
  echo "No Changeset was added for this package-source change."
  exit 1
fi

printf '%s\n' "$changesets"

Repository: chrisdoc/hevy-mcp

Length of output: 210


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'changed packages/node/src/utils/metrics.ts?\n'
git diff --name-only origin/main...HEAD -- packages/node/src/utils/metrics.ts || true

printf '\nnew/changed .changeset files vs origin/main?\n'
git diff --name-status origin/main...HEAD -- .changeset || true

printf '\nall .changeset files currently tracked?\n'
git ls-files '.changeset/*' || true

Repository: chrisdoc/hevy-mcp

Length of output: 562


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'All .changeset entries:\n'
for f in .changeset/*.md; do
  echo "--- $f"
  sed -n '1,240p' "$f"
done

Repository: chrisdoc/hevy-mcp

Length of output: 1236


Add a non-empty Changeset for this package change.

packages/node/src/utils/metrics.ts adds runtime-visible telemetry behavior, but the current changeset contents do not cover it. Add and stage an appropriate non-empty .changeset/*.md entry before merge.

🤖 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/metrics.ts` around lines 16 - 19, Add and stage a
non-empty Changeset entry for the package containing
packages/node/src/utils/metrics.ts, documenting the runtime-visible telemetry
change introduced by activityInvocations. Use the repository’s existing
Changeset format and select the appropriate release impact for that package.

Source: Coding guidelines

Comment on lines +69 to +82
const kind = invocation.kind ?? "tool";
const isPrompt = kind === "prompt";
const sessionId = getCurrentMcpSessionId();
const attributes: Record<string, AttributeValue> = {
"mcp.span.category": DISCOVERY_TOOL_NAMES.has(invocation.name)
? "discovery"
: "tool",
[isPrompt ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
"mcp.operation.kind": invocation.kind ?? "tool",
"mcp.span.category":
kind === "tool" && DISCOVERY_TOOL_NAMES.has(invocation.name)
? "discovery"
: kind,
[kind === "prompt"
? "mcp.prompt.name"
: kind === "resource"
? "mcp.resource.name"
: "mcp.tool.name"]: invocation.name,
"mcp.operation.kind": kind,

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use resource-specific attributes for resource errors.

Lines 77-81 write mcp.resource.name for resource spans. Resource failures still write mcp.tool.name at Line 318, Line 354, and Line 368. Resource error diagnostics will therefore appear as tool operations.

Use one kind-specific name-attribute mapping for span attributes, failure events, and exception attributes. Add a resource failure test.

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

In `@packages/node/src/utils/tool-observer.ts` around lines 69 - 82, The resource
error paths currently use the tool name attribute instead of the
resource-specific attribute. In the observer implementation, reuse one
kind-specific name-attribute mapping for the main span attributes and all
failure/exception handling paths around the resource error sites, including the
logic near lines 318, 354, and 368; add a test verifying resource failures
record mcp.resource.name rather than mcp.tool.name.

Comment on lines +256 to +264
const userHash = getTelemetryUserHash();
const activityKind = invocation.kind ?? "tool";
const activityMetrics = userHash
? { activity_kind: activityKind, user_hash: userHash }
: undefined;
bestEffort(() => toolInvocations.add(1, metrics));
if (activityMetrics) {
bestEffort(() => activityInvocations.add(1, activityMetrics));
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Keep resource reads out of legacy tool metrics.

Line 254 calls recordMcpToolInvocation, and Line 261 increments mcp.tool.invocations for every observation scope. Resource handlers now create these scopes. The existing tool invocation, outcome, error, and duration metrics will include resource reads, which changes the existing request-volume and error panels.

Compute activityKind before Line 254. Skip legacy tool* metric emission for resource operations. Continue to emit mcp.user.activity for all activity kinds.

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

In `@packages/node/src/utils/tool-observer.ts` around lines 256 - 264, Update the
observation flow around recordMcpToolInvocation to compute activityKind before
that call and identify resource operations. Skip all legacy tool
metrics—invocation, outcome, error, and duration—for resource activity, while
continuing to emit mcp.user.activity for every activity kind.

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Resources counted in mcp.tool.* 📎 Requirement gap ≡ Correctness
Description
registerHevyResources() now routes resource reads through the shared ToolObserver, but the Node
observer records every observed invocation into the existing
mcp.tool.invocations/outcome/duration/error metrics and increments session tool-call counts
regardless of invocation kind. This will mix resource traffic into tool reliability/session-shape
metrics and change request-volume and error dashboard semantics, violating the requirement that
those dashboards remain unchanged.
Code

packages/core/src/resources/hevy.ts[R60-63]

+		try {
+			scope = memoizeObservationScope(
+				observer?.start({ name, kind: "resource" }),
+			);
Evidence
Rule 8 requires request-volume and error dashboards to remain unchanged, yet the PR introduces
resource observation by starting the observer with observer?.start({ name, kind: "resource" })
during resource registration. In Node, createNodeToolObserver().start() unconditionally records
observations via recordMcpToolInvocation() and emits the existing mcp.tool.* instruments
(invocations/outcomes/duration/errors) for all invocation kinds, and also increments session
tool-call counters, so these new kind: "resource" observations will be counted as tool traffic and
thereby alter the existing dashboards/queries.

Existing request-volume and error dashboards remain unchanged
packages/core/src/resources/hevy.ts[52-63]
packages/node/src/utils/tool-observer.ts[252-264]
packages/core/src/server.ts[61-75]
packages/core/src/resources/hevy.ts[52-75]
packages/node/src/utils/tool-observer.ts[249-265]
packages/node/src/utils/tool-observer.ts[401-419]
packages/node/src/utils/mcp-session-observability.ts[158-169]

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

## Issue description
Resource reads are now observed via the shared `ToolObserver` (using `kind: "resource"`), but `createNodeToolObserver()` currently records *all* observed invocations into the existing `mcp.tool.*` metrics (and session tool-call counts) regardless of `invocation.kind`. This pollutes tool reliability/session-shape metrics with resource activity and changes request-volume/error dashboard semantics, which must remain unchanged.

## Issue Context
The PR wires resource reads into the observer (e.g., `registerHevyResources(server, runtime, options.observer)` and resource reads calling `observer.start({ name, kind: "resource" })`) to support activity-only DAU/WAU/MAU. However, Node’s observer treats every invocation as a tool for metrics/session accounting by always calling `recordMcpToolInvocation()` and emitting `toolInvocations/toolOutcomes/toolDuration/toolErrors`, so resource observations will be counted as tools and will also increment `session.toolCalls`.

## Fix Focus Areas
- packages/core/src/resources/hevy.ts[52-89]
- packages/core/src/server.ts[71-75]
- packages/node/src/utils/tool-observer.ts[249-265]
- packages/node/src/utils/tool-observer.ts[252-294]
- packages/node/src/utils/tool-observer.ts[401-419]

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


2. Resource errors marked success 🐞 Bug ◔ Observability
Description
withResourceObservation always finishes with outcome: "success" and result.isError: false,
even when the resource read fails. Because readResource converts thrown exceptions into an error
payload, the observation wrapper never sees an exception and misclassifies failures as successes.
Code

packages/core/src/resources/hevy.ts[R73-76]

+				outcome: "success",
+				durationMs: Date.now() - startedAt,
+				result: {
+					isError: false,
Evidence
readResource converts thrown errors into a normal (resolved) ReadResourceResult, so
withResourceObservation's catch block is not taken. The wrapper then always finishes as success
and hard-codes isError: false, misreporting failures.

packages/core/src/resources/hevy.ts[41-49]
packages/core/src/resources/hevy.ts[68-81]

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

## Issue description
Resource handlers wrap failures into a returned `ReadResourceResult` via `readResource(...)`, but `withResourceObservation(...)` treats any returned result as success and sets `isError: false`. This makes resource failure telemetry indistinguishable from success.

## Issue Context
- `readResource` catches exceptions and returns `createResourceErrorResult(uri, error)`.
- `withResourceObservation` only reports errors if the handler throws; it does not detect returned error payloads.

## Fix Focus Areas
- packages/core/src/resources/hevy.ts[41-49]
- packages/core/src/resources/hevy.ts[68-88]

## Suggested fix
Option A (recommended): Let `withResourceObservation` own error conversion.
1. Remove the inner `readResource(...)` try/catch usage from each registered resource handler.
2. In `withResourceObservation`, wrap `handler(uri, context)` in `try/catch`.
3. On success: `finish({ outcome: "success", ... , result: { isError: false, ... } })`.
4. On caught error: call `finish({ outcome: "returned_error" /* or thrown_error */, durationMs, result: { isError: true, ... } })`, then `return createResourceErrorResult(uri, error)` (do not rethrow).

Option B: If you keep `readResource`, add a reliable error signal to its return value so `withResourceObservation` can detect and report `returned_error` + `isError: true` when it returns an error payload.

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


Grey Divider

Context used

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

Qodo Logo

Comment on lines +60 to +63
try {
scope = memoizeObservationScope(
observer?.start({ name, kind: "resource" }),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Resources counted in mcp.tool.* 📎 Requirement gap ≡ Correctness

registerHevyResources() now routes resource reads through the shared ToolObserver, but the Node
observer records every observed invocation into the existing
mcp.tool.invocations/outcome/duration/error metrics and increments session tool-call counts
regardless of invocation kind. This will mix resource traffic into tool reliability/session-shape
metrics and change request-volume and error dashboard semantics, violating the requirement that
those dashboards remain unchanged.
Agent Prompt
## Issue description
Resource reads are now observed via the shared `ToolObserver` (using `kind: "resource"`), but `createNodeToolObserver()` currently records *all* observed invocations into the existing `mcp.tool.*` metrics (and session tool-call counts) regardless of `invocation.kind`. This pollutes tool reliability/session-shape metrics with resource activity and changes request-volume/error dashboard semantics, which must remain unchanged.

## Issue Context
The PR wires resource reads into the observer (e.g., `registerHevyResources(server, runtime, options.observer)` and resource reads calling `observer.start({ name, kind: "resource" })`) to support activity-only DAU/WAU/MAU. However, Node’s observer treats every invocation as a tool for metrics/session accounting by always calling `recordMcpToolInvocation()` and emitting `toolInvocations/toolOutcomes/toolDuration/toolErrors`, so resource observations will be counted as tools and will also increment `session.toolCalls`.

## Fix Focus Areas
- packages/core/src/resources/hevy.ts[52-89]
- packages/core/src/server.ts[71-75]
- packages/node/src/utils/tool-observer.ts[249-265]
- packages/node/src/utils/tool-observer.ts[252-294]
- packages/node/src/utils/tool-observer.ts[401-419]

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

Comment on lines +73 to +76
outcome: "success",
durationMs: Date.now() - startedAt,
result: {
isError: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Resource errors marked success 🐞 Bug ◔ Observability

withResourceObservation always finishes with outcome: "success" and result.isError: false,
even when the resource read fails. Because readResource converts thrown exceptions into an error
payload, the observation wrapper never sees an exception and misclassifies failures as successes.
Agent Prompt
## Issue description
Resource handlers wrap failures into a returned `ReadResourceResult` via `readResource(...)`, but `withResourceObservation(...)` treats any returned result as success and sets `isError: false`. This makes resource failure telemetry indistinguishable from success.

## Issue Context
- `readResource` catches exceptions and returns `createResourceErrorResult(uri, error)`.
- `withResourceObservation` only reports errors if the handler throws; it does not detect returned error payloads.

## Fix Focus Areas
- packages/core/src/resources/hevy.ts[41-49]
- packages/core/src/resources/hevy.ts[68-88]

## Suggested fix
Option A (recommended): Let `withResourceObservation` own error conversion.
1. Remove the inner `readResource(...)` try/catch usage from each registered resource handler.
2. In `withResourceObservation`, wrap `handler(uri, context)` in `try/catch`.
3. On success: `finish({ outcome: "success", ... , result: { isError: false, ... } })`.
4. On caught error: call `finish({ outcome: "returned_error" /* or thrown_error */, durationMs, result: { isError: true, ... } })`, then `return createResourceErrorResult(uri, error)` (do not rethrow).

Option B: If you keep `readResource`, add a reliable error signal to its return value so `withResourceObservation` can detect and report `returned_error` + `isError: true` when it returns an error payload.

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

@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 1.59kB (0.62%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
hevy-mcp-esm 260.26kB 1.59kB (0.62%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: hevy-mcp-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
src-DRt5xk9G.mjs (New) 258.06kB 258.06kB 100.0% 🚀
src-DZjXdluF.mjs (Deleted) -256.46kB 0 bytes -100.0% 🗑️

Files in src-DRt5xk9G.mjs:

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

  • ./src/utils/metrics.ts → Total Size: 2.5kB

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

@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.42857% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.21%. Comparing base (d632c55) to head (e4f78d7).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/node/src/utils/tool-observer.ts 55.55% 0 Missing and 4 partials ⚠️
packages/core/src/resources/hevy.ts 81.25% 3 Missing ⚠️
packages/node/src/utils/telemetry.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #918      +/-   ##
==========================================
- Coverage   83.28%   83.21%   -0.07%     
==========================================
  Files          83       83              
  Lines        5640     5659      +19     
  Branches     1577     1582       +5     
==========================================
+ Hits         4697     4709      +12     
- Misses        435      439       +4     
- Partials      508      511       +3     

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

@charliecreates

charliecreates Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Charlie could not safely repair the merge conflict.

I re-fetched PR #918 at head e4f78d7d0ad9fed13f06af0c13caec004061177f and attempted a merge-based repair against current main 2443cad60f74c21ec38592d2083e7fac1398401a. The merge conflicts in docs/telemetry-data-dictionary.md, packages/node/src/utils/telemetry.ts, packages/node/src/utils/tool-observer.ts, and its test. The PR's active-user metric requires the API-key-derived user_hash path (setTelemetryUser/getTelemetryUserHash), while current main removed that path in the redesigned Sentry/OTel privacy model.

I stopped without pushing because choosing whether to restore that identity path or redesign active-user counting is a product/security decision, not mechanical conflict resolution. Please reconcile the telemetry/privacy contract, then update this PR branch. No branch update was made.

— charlied/pr-merge-conflict-repair

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.

fix: define DAU WAU and MAU from MCP activity only

1 participant