Skip to content

fix(mcp): stop insight-update from reporting success when nothing persists - #108314

Closed
posthog[bot] wants to merge 3 commits into
masterfrom
posthog-self-driving/fixmcp-make-insight-update-persist-34e547
Closed

posthog[bot] wants to merge 3 commits into
masterfrom
posthog-self-driving/fixmcp-make-insight-update-persist-34e547

Conversation

@posthog

@posthog posthog Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  • An agent that calls insight-update can get a success response while no requested field changes. The agent cannot tell that the edit failed, so the task stays blocked.
  • The generated schema drops keys it does not declare. A nested payload ({"id": 7, "insight": {"name": ...}}) or a misnamed field (title) reaches the handler as {id} only.
  • The handler then sends an empty PATCH. The API saves the row (it forces saved=True, is_sample=False) and returns it with only updated_at changed.

Origin

  • Scout: a custom scout
  • First signal: 2026-09-29
  • Inbox report: open
  • Task started by: auto-start, after the report was rated P2 and ready to fix

Changes

  • A hand-written override of insight-update wraps the generated handler, the same way update-feature-flag does.
  • Empty patch: the tool now returns a validation error, sends no PATCH, and names the accepted top-level fields.
  • Round-trip check: after the PATCH, the tool compares the response with the requested name, description, favorited, tags, and dashboards. It returns an error that names the fields that did not persist.
  • The comparison uses the API's normalization: it trims text, and it lowercases, deduplicates, and ignores the order of tags. It ignores the order of dashboard ids.
  • It reads dashboard ids from dashboard_tiles, because the response can omit the deprecated dashboards field. It does not compare query, because the API normalizes the stored query.

Note

Nothing changes on the backend. An empty PATCH from a non-MCP caller still changes updated_at. The MCP tool no longer sends one.

How did you test this code?

  • New services/mcp/tests/unit/insight-update-persistence.test.ts. It sends the input through the tool's own schema, so the tests see what zod strips:
    • An id-only call, a nested payload, and a misnamed field get a rejection, and the tool sends no request. Before this change, all three returned success.
    • A response that does not hold the requested name and tags gets an error that names both fields.
    • A response that matches after API normalization (trimmed name, lowercased and reordered tags and dashboards) returns success.
  • tool-name-validation.test.ts: insight-update is now on the list of generated tools that the override shadows on purpose.
  • Ran the full services/mcp unit suite locally. The repo-wide typecheck failed only on missing react and @posthog/quill packages in the sandbox. None of those errors are in the changed files.
  • No manual test against a live MCP server.

Release status

  • No feature flag controls this change
  • This change is behind a feature flag and is not available to users
  • This change makes a previously flagged feature available to everyone

Automatic notifications

  • Publish to changelog?

Docs update

None.

🤖 Agent context

Autonomy: Fully autonomous

Agent: PostHog Desktop (Claude Code), Claude Opus 5.5 (claude-opus-5-5)

  • Skills: /writing-tests.
  • The duplicate search (gh pr list --search "insight-update mcp") found no open PR for this symptom.
  • The backend serializer and the MCP transport (Content-Type, body encoding) were checked first. Both handle a non-empty PATCH correctly. The empty body from key stripping was the only path that matches "only updated_at changed, no error".
  • A strict schema was considered too, to reject unknown keys up front. Codegen has no strict option, and wrapping the generated preprocess pipe was fragile, so the override checks the patch after parsing instead.
  • No customer data is in this PR. The test fixtures are invented.

Created with PostHog Desktop from this inbox report.

🤖 Generated with Claude Code

…sists

The insight-update schema drops keys it does not declare. A nested or misnamed
field reached the API as an empty PATCH, which saved the row, changed only
updated_at, and returned success.

A hand-written override now rejects a call with no field to change, and it
checks the saved insight against the requested name, description, favorited,
tags, and dashboards before it returns success.

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

Generated-By: PostHog Desktop
Task-Id: 52fe957e-c02e-4ed1-b620-cbebd01a7b05
@posthog posthog Bot added the self-driving label Sep 29, 2026
@posthog
posthog Bot marked this pull request as ready for review September 29, 2026 12:41
@trunk-io

trunk-io Bot commented Sep 29, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

✅ Trunk lane — non-backend lane (svc:mcp)

This PR is assigned to the non-backend lane (svc:mcp). It does not run backend Python tests and may merge in parallel with PRs in other lanes.

✅ Duplication (Python) — clean

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

✅ Duplication (TypeScript) — clean

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

🚨 Comment density — 10% of added code lines are comments (18 of 182)

This section warns when comments are more than 3% of the code lines a PR adds, and alerts above 6%. Before agent-assisted PRs, the typical share was about 2%. Only full-line comments count. Docstrings, generated files, snapshots, migrations, and workflow files are left out.

Comments that restate the code, record how the change came about, or narrate the next line add noise for the next reader. Keep the comments that explain a reason the code cannot show, and remove the rest. See .agents/skills/writing-code-comments/SKILL.md for the house rules.

Files with the most added comment lines:

File Comment lines Added lines
services/mcp/src/tools/insights/update.ts 17 91
services/mcp/tests/unit/insight-update-persistence.test.ts 1 88

This check does not block merging. It updates on every push and clears when the share drops.

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

Generated-By: PostHog Desktop
Task-Id: 52fe957e-c02e-4ed1-b620-cbebd01a7b05
stamphog[bot]

This comment was marked as outdated.

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 4a15287e-c0a5-4ab3-a469-6dd447797a33

📥 Commits

Reviewing files that changed from the base of the PR and between c529765 and 0e3df84.

📒 Files selected for processing (2)
  • services/mcp/src/tools/insights/update.ts
  • services/mcp/tests/unit/insight-update-persistence.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The MCP tool registry now uses a wrapper for insight-update. The wrapper rejects requests without a defined updatable field and checks selected saved values against the request. It normalizes text, tags, and dashboard IDs for comparison. The wrapper returns the generated result when the checked values match and throws an error when they differ. Unit tests cover input validation, mismatches, normalized values, PATCH requests, and tool-name validation.

Priority: ⚪ Not assessed

Merge Risk: ⚪ Minimal · up to 0e3df

Dashboard-only updates are checked against the returned tiles before success is reported. No actionable merge-blocking risk remains after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 0e3df

The change prevents empty insight updates from reporting success and retains the existing project-scoped update path. Its main remaining risk is that an error can be returned after a change has already been saved, making recovery important.

Retained concerns

  • Low · reliability · inferred: A persistence mismatch returns an error after the PATCH may have saved other requested fields. A subsequent unconditional retry, particularly of full-replacement dashboard membership, could overwrite an intervening edit; the error’s instruction to read current state mitigates but does not enforce safe recovery.
Security review details

Security Blast Radius

  • inferred — The changed reporting behavior is limited to calls through the registered MCP insight-update tool; the inspected write path remains project-scoped. No broader credential or authorization transition was identified in that path.

Trust Boundaries and Controls

  • observed — The pre-write guard checks parsed top-level fields, preventing stripped nested or misnamed inputs from reaching the generated handler as an empty PATCH.

Resilience and Maintainability Implications

  • inferred — A response omitting requested tags or both dashboard representations would leave that field unverified while allowing success. Whether the live API produces those response shapes remains unestablished; this conditional success behavior does not establish a new security exposure relative to the former unconditional success response.

Hardening Proposals

  • proposed — If callers require a stronger persistence guarantee, define behavior for omitted response fields and a recovery protocol for post-write errors and concurrent full-replacement edits.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description is complete and follows the repository template. It explains the problem, user-visible changes, testing performed, release status, documentation status, agent context, duplicate search…
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
services/mcp/tests/unit/insight-update-persistence.test.ts (1)

77-81: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the fields sent in the PATCH body.

The response mock returns matching values independently of the request. The test can pass if the PATCH body omits name, tags, favorited, or dashboards. Assert these fields alongside the method and path.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 72694a85-6548-4476-a0ff-4dd94274fe37

📥 Commits

Reviewing files that changed from the base of the PR and between eabc70a and c529765.

📒 Files selected for processing (4)
  • services/mcp/src/tools/index.ts
  • services/mcp/src/tools/insights/update.ts
  • services/mcp/tests/unit/insight-update-persistence.test.ts
  • services/mcp/tests/unit/tool-name-validation.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 3 remain after this review.

Comment on lines +58 to +60
params.dashboards !== undefined &&
Array.isArray(saved.dashboards) &&
JSON.stringify(sortedIds(params.dashboards)) !== JSON.stringify(sortedIds(saved.dashboards))

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

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,115p' services/mcp/src/tools/insights/update.ts
sed -n '385,470p' services/mcp/src/tools/generated/product_analytics.ts
rg -n 'include.*dashboard|dashboards.*include|session.*dashboard|insight.*dashboard' services/mcp/src/generated/product_analytics/api.ts | head -65

Repository: PostHog/posthog

Length of output: 12292


🏁 Script executed:

sed -n '60,125p' services/mcp/src/generated/product_analytics/api.ts
sed -n '150,220p' services/mcp/src/generated/product_analytics/api.ts
sed -n '360,425p' services/mcp/src/generated/product_analytics/api.ts
rg -n -A18 -B8 'function omitResponseFields|const omitResponseFields|omitResponseFields' services/mcp/src
rg -n -A12 -B8 'include_dashboards' services/mcp/src/tools/generated services/mcp/src/generated | head -160

Repository: PostHog/posthog

Length of output: 41746


🏁 Script executed:

rg -n -A20 -B12 'InsightsPartialUpdateParams|InsightUpdateSchema|include_dashboards|InsightRetrieveSchema' services/mcp/src/generated/product_analytics/api.ts services/mcp/src/tools/generated/product_analytics.ts
rg -n -A18 -B8 'insight-get|insightGet|include_dashboards' services/mcp/src/tools/generated/product_analytics.ts services/mcp/src/tools/insights services/mcp/src/generated/product_analytics

Repository: PostHog/posthog

Length of output: 41696


🏁 Script executed:

rg -n -A12 -B12 'include_dashboards|class .*Insight.*Serializer|tags.*Serializer|tags =|get_tags' posthog products api 2>/dev/null | head -240

Repository: PostHog/posthog

Length of output: 18246


🏁 Script executed:

rg -l 'INSIGHT_DASHBOARDS_OPT_IN_ENFORCED|include_dashboards' posthog products | head -40
rg -n -A10 -B10 'INSIGHT_DASHBOARDS_OPT_IN_ENFORCED|include_dashboards' posthog/api posthog/insights products/analytics products/insights 2>/dev/null | head -240

Repository: PostHog/posthog

Length of output: 486


🏁 Script executed:

rg -n -A18 -B18 'include_dashboards|dashboards|tags' products/product_analytics/backend/presentation/insight.py products/product_analytics/backend/tests/api/test_deprecated_dashboards_field.py

Repository: PostHog/posthog

Length of output: 42626


Read back dashboards before reporting success.

The session-authenticated PATCH response omits dashboards, and the generated insight-update request does not pass include_dashboards=true. A dashboard-only update can therefore skip the current comparison and report success without validating persistence. Read back the insight through insight-get with include_dashboards=true, then compare the returned dashboard IDs before returning success.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 0e3df84. The check reads dashboard ids from dashboard_tiles (non-deleted tiles). The response always includes this field, and the backend clears its tile cache after the update. So no second read is needed. It uses dashboards only when dashboard_tiles is not present. A new test case covers a dashboards mismatch with no dashboards field in the response. The success test now asserts the PATCH body.

The PATCH response can omit the deprecated `dashboards` field, so a
dashboards-only update skipped the persistence check. The check now reads
dashboard ids from `dashboard_tiles`, which the response always includes.
The test also asserts the PATCH body.

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

Generated-By: PostHog Desktop
Task-Id: 52fe957e-c02e-4ed1-b620-cbebd01a7b05
@stamphog
stamphog Bot dismissed their stale review September 29, 2026 12:58

A new stamphog review started for this PR — the fresh verdict replaces this approval.

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approved.

This is a contained MCP tool wrapper that follows the existing update-feature-flag override pattern. It sits outside risky territory, has focused tests, and can be reverted easily. The generated tool it wraps exists.

  • Minor: the post-PATCH comparison could report an error for a change that did save, for example if the API filters dashboards the caller cannot access. This affects only the MCP tool's messaging, so it is not blocking.
  • CodeRabbit nitpick, non-blocking: the success test already asserts the PATCH body fields.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✓ no deny categories matched
size ✓ 102L, 2F substantive, 205L/4F incl. docs/generated/snapshots — within ceiling
tier ✓ T1-agent / T1c-medium (205L, 4F, single-area, fix)
stamphog 2.3.0 .stamphog/policy.yml @ 0e3df84 · reviewed head 0e3df84

@trunk-io

trunk-io Bot commented Sep 29, 2026

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

View Full Report ↗︎ ⋅ Docs

@posthog

posthog Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor Author

Reply to the stamphog note about false errors on dashboards: no change is needed. _update_insight_dashboards does not drop dashboards the caller cannot access. It returns a 403 (CannotAddToDashboard / CannotRemoveFromDashboard) or a 400 (DashboardNotFound), and the tool passes that error on. The response serializes dashboard_tiles from all tiles on the insight, with no access filter. The check ignores only deleted tiles. So a dashboards change that saved matches the response.

@scheduled-actions-posthog

Copy link
Copy Markdown
Contributor

This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, please remove the stale label – otherwise this will be closed in another week. If you want to permanently keep it open, use the waiting label.

@scheduled-actions-posthog

Copy link
Copy Markdown
Contributor

This PR was closed due to lack of activity. Feel free to reopen if it's still relevant.

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