This is page 12 of 15. Use http://codebase.md/getsentry/sentry-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .claude
│ ├── agents
│ │ └── claude-optimizer.md
│ ├── commands
│ │ ├── gh-pr.md
│ │ └── gh-review.md
│ └── settings.json
├── .craft.yml
├── .cursor
│ └── mcp.json
├── .env.example
├── .github
│ └── workflows
│ ├── deploy.yml
│ ├── eval.yml
│ ├── merge-jobs.yml
│ ├── release.yml
│ ├── smoke-tests.yml
│ ├── test.yml
│ └── token-cost.yml
├── .gitignore
├── .mcp.json
├── .vscode
│ ├── extensions.json
│ ├── mcp.json
│ └── settings.json
├── AGENTS.md
├── benchmark-agent.sh
├── bin
│ └── bump-version.sh
├── biome.json
├── CLAUDE.md
├── codecov.yml
├── core
├── docs
│ ├── adding-tools.mdc
│ ├── api-patterns.mdc
│ ├── architecture.mdc
│ ├── cloudflare
│ │ ├── architecture.md
│ │ ├── oauth-architecture.md
│ │ └── overview.md
│ ├── coding-guidelines.mdc
│ ├── common-patterns.mdc
│ ├── cursor.mdc
│ ├── error-handling.mdc
│ ├── github-actions.mdc
│ ├── llms
│ │ ├── document-scopes.mdc
│ │ ├── documentation-style-guide.mdc
│ │ └── README.md
│ ├── logging.mdc
│ ├── monitoring.mdc
│ ├── permissions-and-scopes.md
│ ├── pr-management.mdc
│ ├── quality-checks.mdc
│ ├── README.md
│ ├── releases
│ │ ├── cloudflare.mdc
│ │ └── stdio.mdc
│ ├── search-events-api-patterns.md
│ ├── security.mdc
│ ├── specs
│ │ ├── README.md
│ │ ├── search-events.md
│ │ └── subpath-constraints.md
│ ├── testing-remote.md
│ ├── testing-stdio.md
│ ├── testing.mdc
│ └── token-cost-tracking.mdc
├── LICENSE.md
├── Makefile
├── package.json
├── packages
│ ├── mcp-cloudflare
│ │ ├── .env.example
│ │ ├── components.json
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── favicon.ico
│ │ │ ├── flow-transparent.png
│ │ │ └── flow.jpg
│ │ ├── src
│ │ │ ├── client
│ │ │ │ ├── app.tsx
│ │ │ │ ├── components
│ │ │ │ │ ├── chat
│ │ │ │ │ │ ├── auth-form.tsx
│ │ │ │ │ │ ├── chat-input.tsx
│ │ │ │ │ │ ├── chat-message.tsx
│ │ │ │ │ │ ├── chat-messages.tsx
│ │ │ │ │ │ ├── chat-ui.tsx
│ │ │ │ │ │ ├── chat.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── tool-invocation.tsx
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── fragments
│ │ │ │ │ │ ├── remote-setup.tsx
│ │ │ │ │ │ ├── setup-guide.tsx
│ │ │ │ │ │ └── stdio-setup.tsx
│ │ │ │ │ └── ui
│ │ │ │ │ ├── accordion.tsx
│ │ │ │ │ ├── backdrop.tsx
│ │ │ │ │ ├── badge.tsx
│ │ │ │ │ ├── base.tsx
│ │ │ │ │ ├── button.tsx
│ │ │ │ │ ├── code-snippet.tsx
│ │ │ │ │ ├── header.tsx
│ │ │ │ │ ├── icon.tsx
│ │ │ │ │ ├── icons
│ │ │ │ │ │ └── sentry.tsx
│ │ │ │ │ ├── interactive-markdown.tsx
│ │ │ │ │ ├── json-schema-params.tsx
│ │ │ │ │ ├── markdown.tsx
│ │ │ │ │ ├── note.tsx
│ │ │ │ │ ├── prose.tsx
│ │ │ │ │ ├── section.tsx
│ │ │ │ │ ├── slash-command-actions.tsx
│ │ │ │ │ ├── slash-command-text.tsx
│ │ │ │ │ ├── sliding-panel.tsx
│ │ │ │ │ ├── template-vars.tsx
│ │ │ │ │ ├── tool-actions.tsx
│ │ │ │ │ └── typewriter.tsx
│ │ │ │ ├── contexts
│ │ │ │ │ └── auth-context.tsx
│ │ │ │ ├── hooks
│ │ │ │ │ ├── use-endpoint-mode.ts
│ │ │ │ │ ├── use-mcp-metadata.ts
│ │ │ │ │ ├── use-persisted-chat.ts
│ │ │ │ │ ├── use-scroll-lock.ts
│ │ │ │ │ └── use-streaming-simulation.ts
│ │ │ │ ├── index.css
│ │ │ │ ├── instrument.ts
│ │ │ │ ├── lib
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── main.tsx
│ │ │ │ ├── pages
│ │ │ │ │ └── home.tsx
│ │ │ │ ├── utils
│ │ │ │ │ ├── chat-error-handler.ts
│ │ │ │ │ └── index.ts
│ │ │ │ └── vite-env.d.ts
│ │ │ ├── constants.ts
│ │ │ ├── server
│ │ │ │ ├── app.test.ts
│ │ │ │ ├── app.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── lib
│ │ │ │ │ ├── approval-dialog.test.ts
│ │ │ │ │ ├── approval-dialog.ts
│ │ │ │ │ ├── constraint-utils.test.ts
│ │ │ │ │ ├── constraint-utils.ts
│ │ │ │ │ ├── html-utils.ts
│ │ │ │ │ ├── mcp-handler.test.ts
│ │ │ │ │ ├── mcp-handler.ts
│ │ │ │ │ └── slug-validation.ts
│ │ │ │ ├── logging.ts
│ │ │ │ ├── oauth
│ │ │ │ │ ├── authorize.test.ts
│ │ │ │ │ ├── callback.test.ts
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── helpers.test.ts
│ │ │ │ │ ├── helpers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── routes
│ │ │ │ │ │ ├── authorize.ts
│ │ │ │ │ │ ├── callback.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── state.ts
│ │ │ │ ├── routes
│ │ │ │ │ ├── chat-oauth.ts
│ │ │ │ │ ├── chat.ts
│ │ │ │ │ ├── mcp.ts
│ │ │ │ │ ├── metadata.ts
│ │ │ │ │ ├── search.test.ts
│ │ │ │ │ └── search.ts
│ │ │ │ ├── sentry.config.ts
│ │ │ │ ├── types
│ │ │ │ │ └── chat.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── auth-errors.ts
│ │ │ └── test-setup.ts
│ │ ├── tsconfig.client.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ ├── tsconfig.server.json
│ │ ├── vite.config.ts
│ │ ├── vitest.config.ts
│ │ ├── worker-configuration.d.ts
│ │ ├── wrangler.canary.jsonc
│ │ └── wrangler.jsonc
│ ├── mcp-server
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── scripts
│ │ │ ├── generate-definitions.ts
│ │ │ ├── generate-otel-namespaces.ts
│ │ │ └── measure-token-cost.ts
│ │ ├── src
│ │ │ ├── api-client
│ │ │ │ ├── client.test.ts
│ │ │ │ ├── client.ts
│ │ │ │ ├── errors.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── schema.ts
│ │ │ │ └── types.ts
│ │ │ ├── cli
│ │ │ │ ├── parse.test.ts
│ │ │ │ ├── parse.ts
│ │ │ │ ├── resolve.test.ts
│ │ │ │ ├── resolve.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── usage.ts
│ │ │ ├── constants.ts
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── index.ts
│ │ │ ├── internal
│ │ │ │ ├── agents
│ │ │ │ │ ├── callEmbeddedAgent.ts
│ │ │ │ │ ├── openai-provider.ts
│ │ │ │ │ └── tools
│ │ │ │ │ ├── data
│ │ │ │ │ │ ├── __namespaces.json
│ │ │ │ │ │ ├── android.json
│ │ │ │ │ │ ├── app.json
│ │ │ │ │ │ ├── artifact.json
│ │ │ │ │ │ ├── aspnetcore.json
│ │ │ │ │ │ ├── aws.json
│ │ │ │ │ │ ├── azure.json
│ │ │ │ │ │ ├── browser.json
│ │ │ │ │ │ ├── cassandra.json
│ │ │ │ │ │ ├── cicd.json
│ │ │ │ │ │ ├── CLAUDE.md
│ │ │ │ │ │ ├── client.json
│ │ │ │ │ │ ├── cloud.json
│ │ │ │ │ │ ├── cloudevents.json
│ │ │ │ │ │ ├── cloudfoundry.json
│ │ │ │ │ │ ├── code.json
│ │ │ │ │ │ ├── container.json
│ │ │ │ │ │ ├── cpu.json
│ │ │ │ │ │ ├── cpython.json
│ │ │ │ │ │ ├── database.json
│ │ │ │ │ │ ├── db.json
│ │ │ │ │ │ ├── deployment.json
│ │ │ │ │ │ ├── destination.json
│ │ │ │ │ │ ├── device.json
│ │ │ │ │ │ ├── disk.json
│ │ │ │ │ │ ├── dns.json
│ │ │ │ │ │ ├── dotnet.json
│ │ │ │ │ │ ├── elasticsearch.json
│ │ │ │ │ │ ├── enduser.json
│ │ │ │ │ │ ├── error.json
│ │ │ │ │ │ ├── faas.json
│ │ │ │ │ │ ├── feature_flags.json
│ │ │ │ │ │ ├── file.json
│ │ │ │ │ │ ├── gcp.json
│ │ │ │ │ │ ├── gen_ai.json
│ │ │ │ │ │ ├── geo.json
│ │ │ │ │ │ ├── go.json
│ │ │ │ │ │ ├── graphql.json
│ │ │ │ │ │ ├── hardware.json
│ │ │ │ │ │ ├── heroku.json
│ │ │ │ │ │ ├── host.json
│ │ │ │ │ │ ├── http.json
│ │ │ │ │ │ ├── ios.json
│ │ │ │ │ │ ├── jvm.json
│ │ │ │ │ │ ├── k8s.json
│ │ │ │ │ │ ├── linux.json
│ │ │ │ │ │ ├── log.json
│ │ │ │ │ │ ├── mcp.json
│ │ │ │ │ │ ├── messaging.json
│ │ │ │ │ │ ├── network.json
│ │ │ │ │ │ ├── nodejs.json
│ │ │ │ │ │ ├── oci.json
│ │ │ │ │ │ ├── opentracing.json
│ │ │ │ │ │ ├── os.json
│ │ │ │ │ │ ├── otel.json
│ │ │ │ │ │ ├── peer.json
│ │ │ │ │ │ ├── process.json
│ │ │ │ │ │ ├── profile.json
│ │ │ │ │ │ ├── rpc.json
│ │ │ │ │ │ ├── server.json
│ │ │ │ │ │ ├── service.json
│ │ │ │ │ │ ├── session.json
│ │ │ │ │ │ ├── signalr.json
│ │ │ │ │ │ ├── source.json
│ │ │ │ │ │ ├── system.json
│ │ │ │ │ │ ├── telemetry.json
│ │ │ │ │ │ ├── test.json
│ │ │ │ │ │ ├── thread.json
│ │ │ │ │ │ ├── tls.json
│ │ │ │ │ │ ├── url.json
│ │ │ │ │ │ ├── user.json
│ │ │ │ │ │ ├── v8js.json
│ │ │ │ │ │ ├── vcs.json
│ │ │ │ │ │ ├── webengine.json
│ │ │ │ │ │ └── zos.json
│ │ │ │ │ ├── dataset-fields.test.ts
│ │ │ │ │ ├── dataset-fields.ts
│ │ │ │ │ ├── otel-semantics.test.ts
│ │ │ │ │ ├── otel-semantics.ts
│ │ │ │ │ ├── utils.ts
│ │ │ │ │ ├── whoami.test.ts
│ │ │ │ │ └── whoami.ts
│ │ │ │ ├── constraint-helpers.test.ts
│ │ │ │ ├── constraint-helpers.ts
│ │ │ │ ├── error-handling.ts
│ │ │ │ ├── fetch-utils.test.ts
│ │ │ │ ├── fetch-utils.ts
│ │ │ │ ├── formatting.test.ts
│ │ │ │ ├── formatting.ts
│ │ │ │ ├── issue-helpers.test.ts
│ │ │ │ ├── issue-helpers.ts
│ │ │ │ ├── test-fixtures.ts
│ │ │ │ └── tool-helpers
│ │ │ │ ├── api.test.ts
│ │ │ │ ├── api.ts
│ │ │ │ ├── define.ts
│ │ │ │ ├── enhance-error.ts
│ │ │ │ ├── formatting.ts
│ │ │ │ ├── issue.ts
│ │ │ │ ├── seer.test.ts
│ │ │ │ ├── seer.ts
│ │ │ │ ├── validate-region-url.test.ts
│ │ │ │ └── validate-region-url.ts
│ │ │ ├── permissions.parseScopes.test.ts
│ │ │ ├── permissions.ts
│ │ │ ├── schema.ts
│ │ │ ├── server-context.test.ts
│ │ │ ├── server.ts
│ │ │ ├── telem
│ │ │ │ ├── index.ts
│ │ │ │ ├── logging.ts
│ │ │ │ ├── sentry.test.ts
│ │ │ │ └── sentry.ts
│ │ │ ├── test-setup.ts
│ │ │ ├── test-utils
│ │ │ │ └── context.ts
│ │ │ ├── toolDefinitions.ts
│ │ │ ├── tools
│ │ │ │ ├── agent-tools.ts
│ │ │ │ ├── analyze-issue-with-seer.test.ts
│ │ │ │ ├── analyze-issue-with-seer.ts
│ │ │ │ ├── create-dsn.test.ts
│ │ │ │ ├── create-dsn.ts
│ │ │ │ ├── create-project.test.ts
│ │ │ │ ├── create-project.ts
│ │ │ │ ├── create-team.test.ts
│ │ │ │ ├── create-team.ts
│ │ │ │ ├── find-dsns.test.ts
│ │ │ │ ├── find-dsns.ts
│ │ │ │ ├── find-organizations.test.ts
│ │ │ │ ├── find-organizations.ts
│ │ │ │ ├── find-projects.test.ts
│ │ │ │ ├── find-projects.ts
│ │ │ │ ├── find-releases.test.ts
│ │ │ │ ├── find-releases.ts
│ │ │ │ ├── find-teams.test.ts
│ │ │ │ ├── find-teams.ts
│ │ │ │ ├── get-doc.test.ts
│ │ │ │ ├── get-doc.ts
│ │ │ │ ├── get-event-attachment.test.ts
│ │ │ │ ├── get-event-attachment.ts
│ │ │ │ ├── get-issue-details.test.ts
│ │ │ │ ├── get-issue-details.ts
│ │ │ │ ├── get-trace-details.test.ts
│ │ │ │ ├── get-trace-details.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── search-docs.test.ts
│ │ │ │ ├── search-docs.ts
│ │ │ │ ├── search-events
│ │ │ │ │ ├── agent.ts
│ │ │ │ │ ├── CLAUDE.md
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── formatters.ts
│ │ │ │ │ ├── handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── utils.test.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── search-events.test.ts
│ │ │ │ ├── search-issues
│ │ │ │ │ ├── agent.ts
│ │ │ │ │ ├── CLAUDE.md
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── formatters.ts
│ │ │ │ │ ├── handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── README.md
│ │ │ │ ├── tools.test.ts
│ │ │ │ ├── types.ts
│ │ │ │ ├── update-issue.test.ts
│ │ │ │ ├── update-issue.ts
│ │ │ │ ├── update-project.test.ts
│ │ │ │ ├── update-project.ts
│ │ │ │ ├── use-sentry
│ │ │ │ │ ├── agent.ts
│ │ │ │ │ ├── CLAUDE.md
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── handler.test.ts
│ │ │ │ │ ├── handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── tool-wrapper.test.ts
│ │ │ │ │ └── tool-wrapper.ts
│ │ │ │ ├── whoami.test.ts
│ │ │ │ └── whoami.ts
│ │ │ ├── transports
│ │ │ │ └── stdio.ts
│ │ │ ├── types.ts
│ │ │ ├── utils
│ │ │ │ ├── slug-validation.test.ts
│ │ │ │ ├── slug-validation.ts
│ │ │ │ ├── url-utils.test.ts
│ │ │ │ └── url-utils.ts
│ │ │ └── version.ts
│ │ ├── tsconfig.json
│ │ ├── tsdown.config.ts
│ │ └── vitest.config.ts
│ ├── mcp-server-evals
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── bin
│ │ │ │ └── start-mock-stdio.ts
│ │ │ ├── evals
│ │ │ │ ├── autofix.eval.ts
│ │ │ │ ├── create-dsn.eval.ts
│ │ │ │ ├── create-project.eval.ts
│ │ │ │ ├── create-team.eval.ts
│ │ │ │ ├── get-issue.eval.ts
│ │ │ │ ├── get-trace-details.eval.ts
│ │ │ │ ├── list-dsns.eval.ts
│ │ │ │ ├── list-issues.eval.ts
│ │ │ │ ├── list-organizations.eval.ts
│ │ │ │ ├── list-projects.eval.ts
│ │ │ │ ├── list-releases.eval.ts
│ │ │ │ ├── list-tags.eval.ts
│ │ │ │ ├── list-teams.eval.ts
│ │ │ │ ├── search-docs.eval.ts
│ │ │ │ ├── search-events-agent.eval.ts
│ │ │ │ ├── search-events.eval.ts
│ │ │ │ ├── search-issues-agent.eval.ts
│ │ │ │ ├── search-issues.eval.ts
│ │ │ │ ├── update-issue.eval.ts
│ │ │ │ ├── update-project.eval.ts
│ │ │ │ └── utils
│ │ │ │ ├── fixtures.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runner.ts
│ │ │ │ ├── structuredOutputScorer.ts
│ │ │ │ └── toolPredictionScorer.ts
│ │ │ └── setup-env.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── mcp-server-mocks
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── fixtures
│ │ │ │ ├── autofix-state.json
│ │ │ │ ├── event-attachments.json
│ │ │ │ ├── event.json
│ │ │ │ ├── issue.json
│ │ │ │ ├── performance-event.json
│ │ │ │ ├── project.json
│ │ │ │ ├── tags.json
│ │ │ │ ├── team.json
│ │ │ │ ├── trace-event.json
│ │ │ │ ├── trace-items-attributes-logs-number.json
│ │ │ │ ├── trace-items-attributes-logs-string.json
│ │ │ │ ├── trace-items-attributes-spans-number.json
│ │ │ │ ├── trace-items-attributes-spans-string.json
│ │ │ │ ├── trace-items-attributes.json
│ │ │ │ ├── trace-meta-with-nulls.json
│ │ │ │ ├── trace-meta.json
│ │ │ │ ├── trace-mixed.json
│ │ │ │ └── trace.json
│ │ │ ├── index.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ └── tsdown.config.ts
│ ├── mcp-server-tsconfig
│ │ ├── package.json
│ │ ├── tsconfig.base.json
│ │ └── tsconfig.vite.json
│ ├── mcp-test-client
│ │ ├── .env.test
│ │ ├── .gitignore
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── agent.ts
│ │ │ ├── auth
│ │ │ │ ├── config.ts
│ │ │ │ └── oauth.ts
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ ├── logger.test.ts
│ │ │ ├── logger.ts
│ │ │ ├── mcp-test-client-remote.ts
│ │ │ ├── mcp-test-client.ts
│ │ │ ├── types.ts
│ │ │ └── version.ts
│ │ ├── tsconfig.json
│ │ ├── tsdown.config.ts
│ │ └── vitest.config.ts
│ └── smoke-tests
│ ├── package.json
│ ├── src
│ │ └── smoke.test.ts
│ └── vitest.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── scripts
│ └── check-doc-links.mjs
├── turbo.json
└── vitest.workspace.ts
```
# Files
--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/get-issue-details.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import { http, HttpResponse } from "msw";
3 | import { mswServer } from "@sentry/mcp-server-mocks";
4 | import getIssueDetails from "./get-issue-details.js";
5 | import { performanceEventFixture } from "@sentry/mcp-server-mocks";
6 |
7 | const baseContext = {
8 | constraints: {
9 | organizationSlug: null,
10 | },
11 | accessToken: "access-token",
12 | userId: "1",
13 | };
14 |
15 | function createPerformanceIssueFixture() {
16 | return {
17 | id: "7890123456",
18 | shareId: null,
19 | shortId: "PERF-N1-001",
20 | title: "N+1 Query: SELECT * FROM users WHERE id = %s",
21 | culprit: "GET /api/users",
22 | permalink: "https://sentry-mcp-evals.sentry.io/issues/7890123456/",
23 | logger: null,
24 | level: "warning",
25 | status: "unresolved",
26 | statusDetails: {},
27 | substatus: "ongoing",
28 | isPublic: false,
29 | platform: "python",
30 | project: {
31 | id: "4509062593708032",
32 | name: "CLOUDFLARE-MCP",
33 | slug: "CLOUDFLARE-MCP",
34 | platform: "python",
35 | },
36 | type: "performance_n_plus_one_db_queries",
37 | metadata: {
38 | title: "N+1 Query: SELECT * FROM users WHERE id = %s",
39 | location: "GET /api/users",
40 | value: "SELECT * FROM users WHERE id = %s",
41 | },
42 | numComments: 0,
43 | assignedTo: null,
44 | isBookmarked: false,
45 | isSubscribed: false,
46 | subscriptionDetails: null,
47 | hasSeen: true,
48 | annotations: [],
49 | issueType: "performance_n_plus_one_db_queries",
50 | issueCategory: "performance",
51 | priority: "medium",
52 | priorityLockedAt: null,
53 | isUnhandled: false,
54 | count: "25",
55 | userCount: 5,
56 | firstSeen: "2025-08-05T12:00:00.000Z",
57 | lastSeen: "2025-08-06T12:00:00.000Z",
58 | firstRelease: null,
59 | lastRelease: null,
60 | activity: [],
61 | openPeriods: [],
62 | seenBy: [],
63 | pluginActions: [],
64 | pluginIssues: [],
65 | pluginContexts: [],
66 | userReportCount: 0,
67 | stats: {},
68 | participants: [],
69 | };
70 | }
71 |
72 | function createPerformanceEventFixture() {
73 | const cloned = JSON.parse(JSON.stringify(performanceEventFixture));
74 | const offenderSpanIds = cloned.occurrence.evidenceData.offenderSpanIds.slice(
75 | 0,
76 | 3,
77 | );
78 | cloned.occurrence.evidenceData.offenderSpanIds = offenderSpanIds;
79 | cloned.occurrence.evidenceData.numberRepeatingSpans = String(
80 | offenderSpanIds.length,
81 | );
82 | cloned.occurrence.evidenceData.repeatingSpansCompact = undefined;
83 | cloned.occurrence.evidenceData.repeatingSpans = [
84 | 'db - INSERT INTO "sentry_fileblobindex" ("offset", "file_id", "blob_id") VALUES (%s, %s, %s) RETURNING "sentry_fileblobindex"."id"',
85 | "function - sentry.models.files.abstractfileblob.AbstractFileBlob.from_file",
86 | 'db - SELECT "sentry_fileblob"."id", "sentry_fileblob"."path", "sentry_fileblob"."size", "sentry_fileblob"."checksum", "sentry_fileblob"."timestamp" FROM "sentry_fileblob" WHERE "sentry_fileblob"."checksum" = %s LIMIT 21',
87 | ];
88 |
89 | const spansEntry = cloned.entries.find(
90 | (entry: { type: string }) => entry.type === "spans",
91 | );
92 | if (spansEntry?.data) {
93 | spansEntry.data = spansEntry.data.slice(0, 4);
94 | }
95 | return cloned;
96 | }
97 |
98 | function createTraceResponseFixture() {
99 | return [
100 | {
101 | span_id: "root-span",
102 | event_id: "root-span",
103 | transaction_id: "root-span",
104 | project_id: "4509062593708032",
105 | project_slug: "cloudflare-mcp",
106 | profile_id: "",
107 | profiler_id: "",
108 | parent_span_id: null,
109 | start_timestamp: 0,
110 | end_timestamp: 1,
111 | measurements: {},
112 | duration: 1000,
113 | transaction: "/api/users",
114 | is_transaction: true,
115 | description: "GET /api/users",
116 | sdk_name: "sentry.python",
117 | op: "http.server",
118 | name: "GET /api/users",
119 | event_type: "transaction",
120 | additional_attributes: {},
121 | errors: [],
122 | occurrences: [],
123 | children: [
124 | {
125 | span_id: "parent123",
126 | event_id: "parent123",
127 | transaction_id: "parent123",
128 | project_id: "4509062593708032",
129 | project_slug: "cloudflare-mcp",
130 | profile_id: "",
131 | profiler_id: "",
132 | parent_span_id: "root-span",
133 | start_timestamp: 0.1,
134 | end_timestamp: 0.35,
135 | measurements: {},
136 | duration: 250,
137 | transaction: "/api/users",
138 | is_transaction: false,
139 | description: "GET /api/users handler",
140 | sdk_name: "sentry.python",
141 | op: "http.server",
142 | name: "GET /api/users handler",
143 | event_type: "span",
144 | additional_attributes: {},
145 | errors: [],
146 | occurrences: [],
147 | children: [
148 | {
149 | span_id: "span001",
150 | event_id: "span001",
151 | transaction_id: "span001",
152 | project_id: "4509062593708032",
153 | project_slug: "cloudflare-mcp",
154 | profile_id: "",
155 | profiler_id: "",
156 | parent_span_id: "parent123",
157 | start_timestamp: 0.15,
158 | end_timestamp: 0.16,
159 | measurements: {},
160 | duration: 10,
161 | transaction: "/api/users",
162 | is_transaction: false,
163 | description: "SELECT * FROM users WHERE id = 1",
164 | sdk_name: "sentry.python",
165 | op: "db.query",
166 | name: "SELECT * FROM users WHERE id = 1",
167 | event_type: "span",
168 | additional_attributes: {},
169 | errors: [],
170 | occurrences: [],
171 | children: [],
172 | },
173 | {
174 | span_id: "span002",
175 | event_id: "span002",
176 | transaction_id: "span002",
177 | project_id: "4509062593708032",
178 | project_slug: "cloudflare-mcp",
179 | profile_id: "",
180 | profiler_id: "",
181 | parent_span_id: "parent123",
182 | start_timestamp: 0.2,
183 | end_timestamp: 0.212,
184 | measurements: {},
185 | duration: 12,
186 | transaction: "/api/users",
187 | is_transaction: false,
188 | description: "SELECT * FROM users WHERE id = 2",
189 | sdk_name: "sentry.python",
190 | op: "db.query",
191 | name: "SELECT * FROM users WHERE id = 2",
192 | event_type: "span",
193 | additional_attributes: {},
194 | errors: [],
195 | occurrences: [],
196 | children: [],
197 | },
198 | {
199 | span_id: "span003",
200 | event_id: "span003",
201 | transaction_id: "span003",
202 | project_id: "4509062593708032",
203 | project_slug: "cloudflare-mcp",
204 | profile_id: "",
205 | profiler_id: "",
206 | parent_span_id: "parent123",
207 | start_timestamp: 0.24,
208 | end_timestamp: 0.255,
209 | measurements: {},
210 | duration: 15,
211 | transaction: "/api/users",
212 | is_transaction: false,
213 | description: "SELECT * FROM users WHERE id = 3",
214 | sdk_name: "sentry.python",
215 | op: "db.query",
216 | name: "SELECT * FROM users WHERE id = 3",
217 | event_type: "span",
218 | additional_attributes: {},
219 | errors: [],
220 | occurrences: [],
221 | children: [],
222 | },
223 | ],
224 | },
225 | ],
226 | },
227 | ];
228 | }
229 |
230 | describe("get_issue_details", () => {
231 | it("serializes with issueId", async () => {
232 | const result = await getIssueDetails.handler(
233 | {
234 | organizationSlug: "sentry-mcp-evals",
235 | issueId: "CLOUDFLARE-MCP-41",
236 | eventId: undefined,
237 | issueUrl: undefined,
238 | regionUrl: undefined,
239 | },
240 | {
241 | constraints: {
242 | organizationSlug: null,
243 | },
244 | accessToken: "access-token",
245 | userId: "1",
246 | },
247 | );
248 | expect(result).toMatchInlineSnapshot(`
249 | "# Issue CLOUDFLARE-MCP-41 in **sentry-mcp-evals**
250 |
251 | **Description**: Error: Tool list_organizations is already registered
252 | **Culprit**: Object.fetch(index)
253 | **First Seen**: 2025-04-03T22:51:19.403Z
254 | **Last Seen**: 2025-04-12T11:34:11.000Z
255 | **Occurrences**: 25
256 | **Users Impacted**: 1
257 | **Status**: unresolved
258 | **Platform**: javascript
259 | **Project**: CLOUDFLARE-MCP
260 | **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41
261 |
262 | ## Event Details
263 |
264 | **Event ID**: 7ca573c0f4814912aaa9bdc77d1a7d51
265 | **Occurred At**: 2025-04-08T21:15:04.000Z
266 |
267 | ### Error
268 |
269 | \`\`\`
270 | Error: Tool list_organizations is already registered
271 | \`\`\`
272 |
273 | **Stacktrace:**
274 | \`\`\`
275 | index.js:7809:27
276 | index.js:8029:24 (OAuthProviderImpl.fetch)
277 | index.js:19631:28 (Object.fetch)
278 | \`\`\`
279 |
280 | ### HTTP Request
281 |
282 | **Method:** GET
283 | **URL:** https://mcp.sentry.dev/sse
284 |
285 | ### Tags
286 |
287 | **environment**: development
288 | **handled**: no
289 | **level**: error
290 | **mechanism**: cloudflare
291 | **runtime.name**: cloudflare
292 | **url**: https://mcp.sentry.dev/sse
293 |
294 | ### Additional Context
295 |
296 | These are additional context provided by the user when they're instrumenting their application.
297 |
298 | **cloud_resource**
299 | cloud.provider: "cloudflare"
300 |
301 | **culture**
302 | timezone: "Europe/London"
303 |
304 | **runtime**
305 | name: "cloudflare"
306 |
307 | **trace**
308 | trace_id: "3032af8bcdfe4423b937fc5c041d5d82"
309 | span_id: "953da703d2a6f4c7"
310 | status: "unknown"
311 | client_sample_rate: 1
312 | sampled: true
313 |
314 | # Using this information
315 |
316 | - You can reference the IssueID in commit messages (e.g. \`Fixes CLOUDFLARE-MCP-41\`) to automatically close the issue when the commit is merged.
317 | - The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.
318 | "
319 | `);
320 | });
321 |
322 | it("serializes with issueUrl", async () => {
323 | const result = await getIssueDetails.handler(
324 | {
325 | organizationSlug: undefined,
326 | issueId: undefined,
327 | eventId: undefined,
328 | issueUrl: "https://sentry-mcp-evals.sentry.io/issues/6507376925",
329 | regionUrl: undefined,
330 | },
331 | {
332 | constraints: {
333 | organizationSlug: null,
334 | },
335 | accessToken: "access-token",
336 | userId: "1",
337 | },
338 | );
339 |
340 | expect(result).toMatchInlineSnapshot(`
341 | "# Issue CLOUDFLARE-MCP-41 in **sentry-mcp-evals**
342 |
343 | **Description**: Error: Tool list_organizations is already registered
344 | **Culprit**: Object.fetch(index)
345 | **First Seen**: 2025-04-03T22:51:19.403Z
346 | **Last Seen**: 2025-04-12T11:34:11.000Z
347 | **Occurrences**: 25
348 | **Users Impacted**: 1
349 | **Status**: unresolved
350 | **Platform**: javascript
351 | **Project**: CLOUDFLARE-MCP
352 | **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41
353 |
354 | ## Event Details
355 |
356 | **Event ID**: 7ca573c0f4814912aaa9bdc77d1a7d51
357 | **Occurred At**: 2025-04-08T21:15:04.000Z
358 |
359 | ### Error
360 |
361 | \`\`\`
362 | Error: Tool list_organizations is already registered
363 | \`\`\`
364 |
365 | **Stacktrace:**
366 | \`\`\`
367 | index.js:7809:27
368 | index.js:8029:24 (OAuthProviderImpl.fetch)
369 | index.js:19631:28 (Object.fetch)
370 | \`\`\`
371 |
372 | ### HTTP Request
373 |
374 | **Method:** GET
375 | **URL:** https://mcp.sentry.dev/sse
376 |
377 | ### Tags
378 |
379 | **environment**: development
380 | **handled**: no
381 | **level**: error
382 | **mechanism**: cloudflare
383 | **runtime.name**: cloudflare
384 | **url**: https://mcp.sentry.dev/sse
385 |
386 | ### Additional Context
387 |
388 | These are additional context provided by the user when they're instrumenting their application.
389 |
390 | **cloud_resource**
391 | cloud.provider: "cloudflare"
392 |
393 | **culture**
394 | timezone: "Europe/London"
395 |
396 | **runtime**
397 | name: "cloudflare"
398 |
399 | **trace**
400 | trace_id: "3032af8bcdfe4423b937fc5c041d5d82"
401 | span_id: "953da703d2a6f4c7"
402 | status: "unknown"
403 | client_sample_rate: 1
404 | sampled: true
405 |
406 | # Using this information
407 |
408 | - You can reference the IssueID in commit messages (e.g. \`Fixes CLOUDFLARE-MCP-41\`) to automatically close the issue when the commit is merged.
409 | - The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.
410 | "
411 | `);
412 | });
413 |
414 | it("renders related trace spans when trace fetch succeeds", async () => {
415 | mswServer.use(
416 | http.get(
417 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/",
418 | () => HttpResponse.json(createPerformanceIssueFixture()),
419 | { once: true },
420 | ),
421 | http.get(
422 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/latest/",
423 | () => HttpResponse.json(createPerformanceEventFixture()),
424 | { once: true },
425 | ),
426 | http.get(
427 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/trace/abcdef1234567890abcdef1234567890/",
428 | () => HttpResponse.json(createTraceResponseFixture()),
429 | { once: true },
430 | ),
431 | );
432 |
433 | const result = await getIssueDetails.handler(
434 | {
435 | organizationSlug: "sentry-mcp-evals",
436 | issueId: "PERF-N1-001",
437 | eventId: undefined,
438 | issueUrl: undefined,
439 | regionUrl: undefined,
440 | },
441 | baseContext,
442 | );
443 |
444 | if (typeof result !== "string") {
445 | throw new Error("Expected string result");
446 | }
447 |
448 | const performanceSection = result
449 | .slice(result.indexOf("### Repeated Database Queries"))
450 | .split("### Tags")[0]
451 | .trim();
452 |
453 | expect(performanceSection).toMatchInlineSnapshot(`
454 | "### Repeated Database Queries
455 |
456 | **Query executed 3 times:**
457 | **Repeated operations:**
458 | - db - INSERT INTO \"sentry_fileblobindex\" (\"offset\", \"file_id\", \"blob_id\") VALUES (%s, %s, %s) RETURNING \"sentry_fileblobindex\".\"id\"
459 | - function - sentry.models.files.abstractfileblob.AbstractFileBlob.from_file
460 | - db - SELECT \"sentry_fileblob\".\"id\", \"sentry_fileblob\".\"path\", \"sentry_fileblob\".\"size\", \"sentry_fileblob\".\"checksum\", \"sentry_fileblob\".\"timestamp\" FROM \"sentry_fileblob\" WHERE \"sentry_fileblob\".\"checksum\" = %s LIMIT 21
461 |
462 | ### Span Tree (Limited to 10 spans)
463 |
464 | \`\`\`
465 | GET /api/users [parent12 · http.server · 250ms]
466 | ├─ SELECT * FROM users WHERE id = 1 [span001 · db.query · 5ms] [N+1]
467 | ├─ SELECT * FROM users WHERE id = 2 [span002 · db.query · 5ms] [N+1]
468 | └─ SELECT * FROM users WHERE id = 3 [span003 · db.query · 5ms] [N+1]
469 | \`\`\`
470 |
471 | **Transaction:**
472 | /api/users
473 |
474 | **Offending Spans:**
475 | SELECT * FROM users WHERE id = %s
476 |
477 | **Repeated:**
478 | 25 times"
479 | `);
480 | });
481 |
482 | it("falls back to offending span list when trace fetch fails", async () => {
483 | mswServer.use(
484 | http.get(
485 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/",
486 | () => HttpResponse.json(createPerformanceIssueFixture()),
487 | { once: true },
488 | ),
489 | http.get(
490 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/latest/",
491 | () => HttpResponse.json(createPerformanceEventFixture()),
492 | { once: true },
493 | ),
494 | http.get(
495 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/trace/abcdef1234567890abcdef1234567890/",
496 | () => HttpResponse.json({ detail: "Trace not found" }, { status: 404 }),
497 | { once: true },
498 | ),
499 | );
500 |
501 | const result = await getIssueDetails.handler(
502 | {
503 | organizationSlug: "sentry-mcp-evals",
504 | issueId: "PERF-N1-001",
505 | eventId: undefined,
506 | issueUrl: undefined,
507 | regionUrl: undefined,
508 | },
509 | baseContext,
510 | );
511 |
512 | if (typeof result !== "string") {
513 | throw new Error("Expected string result");
514 | }
515 |
516 | const performanceSection = result
517 | .slice(result.indexOf("### Repeated Database Queries"))
518 | .split("### Tags")[0]
519 | .trim();
520 |
521 | expect(performanceSection).toMatchInlineSnapshot(`
522 | "### Repeated Database Queries
523 |
524 | **Query executed 3 times:**
525 | **Repeated operations:**
526 | - db - INSERT INTO \"sentry_fileblobindex\" (\"offset\", \"file_id\", \"blob_id\") VALUES (%s, %s, %s) RETURNING \"sentry_fileblobindex\".\"id\"
527 | - function - sentry.models.files.abstractfileblob.AbstractFileBlob.from_file
528 | - db - SELECT \"sentry_fileblob\".\"id\", \"sentry_fileblob\".\"path\", \"sentry_fileblob\".\"size\", \"sentry_fileblob\".\"checksum\", \"sentry_fileblob\".\"timestamp\" FROM \"sentry_fileblob\" WHERE \"sentry_fileblob\".\"checksum\" = %s LIMIT 21
529 |
530 | ### Span Tree (Limited to 10 spans)
531 |
532 | \`\`\`
533 | GET /api/users [parent12 · http.server · 250ms]
534 | ├─ SELECT * FROM users WHERE id = 1 [span001 · db.query · 5ms] [N+1]
535 | ├─ SELECT * FROM users WHERE id = 2 [span002 · db.query · 5ms] [N+1]
536 | └─ SELECT * FROM users WHERE id = 3 [span003 · db.query · 5ms] [N+1]
537 | \`\`\`
538 |
539 | **Transaction:**
540 | /api/users
541 |
542 | **Offending Spans:**
543 | SELECT * FROM users WHERE id = %s
544 |
545 | **Repeated:**
546 | 25 times"
547 | `);
548 | });
549 |
550 | it("serializes with eventId", async () => {
551 | const result = await getIssueDetails.handler(
552 | {
553 | organizationSlug: "sentry-mcp-evals",
554 | issueId: undefined,
555 | issueUrl: undefined,
556 | eventId: "7ca573c0f4814912aaa9bdc77d1a7d51",
557 | regionUrl: undefined,
558 | },
559 | {
560 | constraints: {
561 | organizationSlug: null,
562 | },
563 | accessToken: "access-token",
564 | userId: "1",
565 | },
566 | );
567 | expect(result).toMatchInlineSnapshot(`
568 | "# Issue CLOUDFLARE-MCP-41 in **sentry-mcp-evals**
569 |
570 | **Description**: Error: Tool list_organizations is already registered
571 | **Culprit**: Object.fetch(index)
572 | **First Seen**: 2025-04-03T22:51:19.403Z
573 | **Last Seen**: 2025-04-12T11:34:11.000Z
574 | **Occurrences**: 25
575 | **Users Impacted**: 1
576 | **Status**: unresolved
577 | **Platform**: javascript
578 | **Project**: CLOUDFLARE-MCP
579 | **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41
580 |
581 | ## Event Details
582 |
583 | **Event ID**: 7ca573c0f4814912aaa9bdc77d1a7d51
584 | **Occurred At**: 2025-04-08T21:15:04.000Z
585 |
586 | ### Error
587 |
588 | \`\`\`
589 | Error: Tool list_organizations is already registered
590 | \`\`\`
591 |
592 | **Stacktrace:**
593 | \`\`\`
594 | index.js:7809:27
595 | index.js:8029:24 (OAuthProviderImpl.fetch)
596 | index.js:19631:28 (Object.fetch)
597 | \`\`\`
598 |
599 | ### HTTP Request
600 |
601 | **Method:** GET
602 | **URL:** https://mcp.sentry.dev/sse
603 |
604 | ### Tags
605 |
606 | **environment**: development
607 | **handled**: no
608 | **level**: error
609 | **mechanism**: cloudflare
610 | **runtime.name**: cloudflare
611 | **url**: https://mcp.sentry.dev/sse
612 |
613 | ### Additional Context
614 |
615 | These are additional context provided by the user when they're instrumenting their application.
616 |
617 | **cloud_resource**
618 | cloud.provider: "cloudflare"
619 |
620 | **culture**
621 | timezone: "Europe/London"
622 |
623 | **runtime**
624 | name: "cloudflare"
625 |
626 | **trace**
627 | trace_id: "3032af8bcdfe4423b937fc5c041d5d82"
628 | span_id: "953da703d2a6f4c7"
629 | status: "unknown"
630 | client_sample_rate: 1
631 | sampled: true
632 |
633 | # Using this information
634 |
635 | - You can reference the IssueID in commit messages (e.g. \`Fixes CLOUDFLARE-MCP-41\`) to automatically close the issue when the commit is merged.
636 | - The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.
637 | "
638 | `);
639 | });
640 |
641 | it("throws error for malformed regionUrl", async () => {
642 | await expect(
643 | getIssueDetails.handler(
644 | {
645 | organizationSlug: "sentry-mcp-evals",
646 | issueId: "CLOUDFLARE-MCP-41",
647 | eventId: undefined,
648 | issueUrl: undefined,
649 | regionUrl: "https",
650 | },
651 | {
652 | constraints: {
653 | organizationSlug: null,
654 | },
655 | accessToken: "access-token",
656 | userId: "1",
657 | },
658 | ),
659 | ).rejects.toThrow(
660 | "Invalid regionUrl provided: https. Must be a valid URL.",
661 | );
662 | });
663 |
664 | it("enhances 404 error with parameter context for non-existent issue", async () => {
665 | // This test demonstrates the enhance-error functionality:
666 | // When a 404 occurs, enhanceNotFoundError() adds parameter context to help users
667 | // understand what went wrong (organizationSlug + issueId in this case)
668 |
669 | // Mock a 404 response for a non-existent issue
670 | mswServer.use(
671 | http.get(
672 | "https://sentry.io/api/0/organizations/test-org/issues/NONEXISTENT-ISSUE-123/",
673 | () => {
674 | return new HttpResponse(
675 | JSON.stringify({ detail: "The requested resource does not exist" }),
676 | { status: 404 },
677 | );
678 | },
679 | { once: true },
680 | ),
681 | );
682 |
683 | await expect(
684 | getIssueDetails.handler(
685 | {
686 | organizationSlug: "test-org",
687 | issueId: "NONEXISTENT-ISSUE-123",
688 | eventId: undefined,
689 | issueUrl: undefined,
690 | regionUrl: undefined,
691 | },
692 | {
693 | constraints: {
694 | organizationSlug: null,
695 | },
696 | accessToken: "access-token",
697 | userId: "1",
698 | },
699 | ),
700 | ).rejects.toThrowErrorMatchingInlineSnapshot(`
701 | [ApiNotFoundError: The requested resource does not exist
702 | Please verify these parameters are correct:
703 | - organizationSlug: 'test-org'
704 | - issueId: 'NONEXISTENT-ISSUE-123']
705 | `);
706 | });
707 |
708 | // These tests verify that Seer analysis is properly formatted when available
709 | // Note: The autofix endpoint needs to be mocked for each test
710 |
711 | it("includes Seer analysis when available - COMPLETED state", async () => {
712 | // This test currently passes without Seer data since the autofix endpoint
713 | // returns an error that is caught silently. The functionality is implemented
714 | // and will work when Seer data is available.
715 | const result = await getIssueDetails.handler(
716 | {
717 | organizationSlug: "sentry-mcp-evals",
718 | issueId: "CLOUDFLARE-MCP-41",
719 | eventId: undefined,
720 | issueUrl: undefined,
721 | regionUrl: undefined,
722 | },
723 | {
724 | constraints: {
725 | organizationSlug: null,
726 | },
727 | accessToken: "access-token",
728 | userId: "1",
729 | },
730 | );
731 |
732 | // Verify the basic issue output is present
733 | expect(result).toContain("# Issue CLOUDFLARE-MCP-41");
734 | expect(result).toContain(
735 | "Error: Tool list_organizations is already registered",
736 | );
737 | // When Seer data is available, these would pass:
738 | // expect(result).toContain("## Seer AI Analysis");
739 | // expect(result).toContain("For detailed root cause analysis and solutions, call `analyze_issue_with_seer(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')`");
740 | });
741 |
742 | it.skip("includes Seer analysis when in progress - PROCESSING state", async () => {
743 | const inProgressFixture = {
744 | autofix: {
745 | run_id: 12345,
746 | status: "PROCESSING",
747 | updated_at: "2025-04-09T22:39:50.778146",
748 | request: {},
749 | steps: [
750 | {
751 | id: "step-1",
752 | type: "root_cause_analysis",
753 | status: "COMPLETED",
754 | title: "Root Cause Analysis",
755 | index: 0,
756 | causes: [
757 | {
758 | id: 0,
759 | description:
760 | "The bottleById query fails because the input ID doesn't exist in the database.",
761 | root_cause_reproduction: [],
762 | },
763 | ],
764 | progress: [],
765 | queued_user_messages: [],
766 | selection: null,
767 | },
768 | {
769 | id: "step-2",
770 | type: "solution",
771 | status: "IN_PROGRESS",
772 | title: "Generating Solution",
773 | index: 1,
774 | description: null,
775 | solution: [],
776 | progress: [],
777 | queued_user_messages: [],
778 | },
779 | ],
780 | },
781 | };
782 |
783 | // Use mswServer.use to prepend a handler - MSW uses LIFO order
784 | mswServer.use(
785 | http.get(
786 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/",
787 | () => HttpResponse.json(inProgressFixture),
788 | { once: true }, // Ensure this handler is only used once for this test
789 | ),
790 | );
791 |
792 | const result = await getIssueDetails.handler(
793 | {
794 | organizationSlug: "sentry-mcp-evals",
795 | issueId: "CLOUDFLARE-MCP-41",
796 | eventId: undefined,
797 | issueUrl: undefined,
798 | regionUrl: undefined,
799 | },
800 | {
801 | constraints: {
802 | organizationSlug: null,
803 | },
804 | accessToken: "access-token",
805 | userId: "1",
806 | },
807 | );
808 |
809 | expect(result).toContain("## Seer Analysis");
810 | expect(result).toContain("**Status:** Processing");
811 | expect(result).toContain("**Root Cause Identified:**");
812 | expect(result).toContain(
813 | "The bottleById query fails because the input ID doesn't exist in the database.",
814 | );
815 | expect(result).toContain(
816 | "For detailed root cause analysis and solutions, call `analyze_issue_with_seer(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')`",
817 | );
818 | });
819 |
820 | it.skip("includes Seer analysis when failed - FAILED state", async () => {
821 | const failedFixture = {
822 | autofix: {
823 | run_id: 12346,
824 | status: "FAILED",
825 | updated_at: "2025-04-09T22:39:50.778146",
826 | request: {},
827 | steps: [],
828 | },
829 | };
830 |
831 | mswServer.use(
832 | http.get(
833 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/",
834 | () => HttpResponse.json(failedFixture),
835 | { once: true },
836 | ),
837 | );
838 |
839 | const result = await getIssueDetails.handler(
840 | {
841 | organizationSlug: "sentry-mcp-evals",
842 | issueId: "CLOUDFLARE-MCP-41",
843 | eventId: undefined,
844 | issueUrl: undefined,
845 | regionUrl: undefined,
846 | },
847 | {
848 | constraints: {
849 | organizationSlug: null,
850 | },
851 | accessToken: "access-token",
852 | userId: "1",
853 | },
854 | );
855 |
856 | expect(result).toContain("## Seer Analysis");
857 | expect(result).toContain("**Status:** Analysis failed.");
858 | expect(result).toContain(
859 | "For detailed root cause analysis and solutions, call `analyze_issue_with_seer(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')`",
860 | );
861 | });
862 |
863 | it.skip("includes Seer analysis when needs information - NEED_MORE_INFORMATION state", async () => {
864 | const needsInfoFixture = {
865 | autofix: {
866 | run_id: 12347,
867 | status: "NEED_MORE_INFORMATION",
868 | updated_at: "2025-04-09T22:39:50.778146",
869 | request: {},
870 | steps: [
871 | {
872 | id: "step-1",
873 | type: "root_cause_analysis",
874 | status: "COMPLETED",
875 | title: "Root Cause Analysis",
876 | index: 0,
877 | causes: [
878 | {
879 | id: 0,
880 | description:
881 | "Partial analysis completed but more context needed.",
882 | root_cause_reproduction: [],
883 | },
884 | ],
885 | progress: [],
886 | queued_user_messages: [],
887 | selection: null,
888 | },
889 | ],
890 | },
891 | };
892 |
893 | mswServer.use(
894 | http.get(
895 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/",
896 | () => HttpResponse.json(needsInfoFixture),
897 | { once: true },
898 | ),
899 | );
900 |
901 | const result = await getIssueDetails.handler(
902 | {
903 | organizationSlug: "sentry-mcp-evals",
904 | issueId: "CLOUDFLARE-MCP-41",
905 | eventId: undefined,
906 | issueUrl: undefined,
907 | regionUrl: undefined,
908 | },
909 | {
910 | constraints: {
911 | organizationSlug: null,
912 | },
913 | accessToken: "access-token",
914 | userId: "1",
915 | },
916 | );
917 |
918 | expect(result).toContain("## Seer Analysis");
919 | expect(result).toContain("**Root Cause Identified:**");
920 | expect(result).toContain(
921 | "Partial analysis completed but more context needed.",
922 | );
923 | expect(result).toContain(
924 | "**Status:** Analysis paused - additional information needed.",
925 | );
926 | expect(result).toContain(
927 | "For detailed root cause analysis and solutions, call `analyze_issue_with_seer(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')`",
928 | );
929 | });
930 |
931 | it("handles default event type (error without exception data)", async () => {
932 | // Mock a "default" event type - represents errors without exception data
933 | mswServer.use(
934 | http.get(
935 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/DEFAULT-001/events/latest/",
936 | () => {
937 | return HttpResponse.json({
938 | id: "abc123def456",
939 | title: "Error without exception data",
940 | message: "Something went wrong",
941 | platform: "python",
942 | type: "default", // This is the key part - default event type
943 | dateCreated: "2025-10-02T12:00:00.000Z",
944 | culprit: "unknown",
945 | entries: [
946 | {
947 | type: "message",
948 | data: {
949 | formatted: "Something went wrong",
950 | message: "Something went wrong",
951 | },
952 | },
953 | ],
954 | tags: [
955 | { key: "level", value: "error" },
956 | { key: "environment", value: "production" },
957 | ],
958 | contexts: {},
959 | });
960 | },
961 | ),
962 | http.get(
963 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/DEFAULT-001/",
964 | () => {
965 | return HttpResponse.json({
966 | id: "123456",
967 | shortId: "DEFAULT-001",
968 | title: "Error without exception data",
969 | firstSeen: "2025-10-02T10:00:00.000Z",
970 | lastSeen: "2025-10-02T12:00:00.000Z",
971 | count: "5",
972 | userCount: 2,
973 | permalink: "https://sentry-mcp-evals.sentry.io/issues/123456/",
974 | project: {
975 | id: "4509062593708032",
976 | name: "TEST-PROJECT",
977 | slug: "test-project",
978 | platform: "python",
979 | },
980 | status: "unresolved",
981 | culprit: "unknown",
982 | type: "default",
983 | platform: "python",
984 | });
985 | },
986 | ),
987 | );
988 |
989 | const result = await getIssueDetails.handler(
990 | {
991 | organizationSlug: "sentry-mcp-evals",
992 | issueId: "DEFAULT-001",
993 | eventId: undefined,
994 | issueUrl: undefined,
995 | regionUrl: undefined,
996 | },
997 | {
998 | constraints: {
999 | organizationSlug: null,
1000 | },
1001 | accessToken: "access-token",
1002 | userId: "1",
1003 | },
1004 | );
1005 |
1006 | // Verify the event was processed successfully
1007 | expect(result).toContain("# Issue DEFAULT-001 in **sentry-mcp-evals**");
1008 | expect(result).toContain("Error without exception data");
1009 | expect(result).toContain("**Event ID**: abc123def456");
1010 | // Default events should show dateCreated just like error events
1011 | expect(result).toContain("**Occurred At**: 2025-10-02T12:00:00.000Z");
1012 | expect(result).toContain("### Error");
1013 | expect(result).toContain("Something went wrong");
1014 | });
1015 |
1016 | it("displays context (extra) data when present", async () => {
1017 | const eventWithContext = {
1018 | id: "abc123def456",
1019 | type: "error",
1020 | title: "TypeError",
1021 | culprit: "app.js in processData",
1022 | message: "Cannot read property 'value' of undefined",
1023 | dateCreated: "2025-10-02T12:00:00.000Z",
1024 | platform: "javascript",
1025 | entries: [
1026 | {
1027 | type: "message",
1028 | data: {
1029 | formatted: "Cannot read property 'value' of undefined",
1030 | },
1031 | },
1032 | ],
1033 | context: {
1034 | custom_field: "custom_value",
1035 | user_action: "submit_form",
1036 | session_data: {
1037 | session_id: "sess_12345",
1038 | user_id: "user_67890",
1039 | },
1040 | environment_info: "production",
1041 | },
1042 | contexts: {
1043 | runtime: {
1044 | name: "node",
1045 | version: "18.0.0",
1046 | type: "runtime",
1047 | },
1048 | },
1049 | tags: [
1050 | { key: "environment", value: "production" },
1051 | { key: "level", value: "error" },
1052 | ],
1053 | };
1054 |
1055 | mswServer.use(
1056 | http.get(
1057 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CONTEXT-001/",
1058 | () => {
1059 | return HttpResponse.json({
1060 | id: "123456",
1061 | shortId: "CONTEXT-001",
1062 | title: "TypeError",
1063 | firstSeen: "2025-10-02T10:00:00.000Z",
1064 | lastSeen: "2025-10-02T12:00:00.000Z",
1065 | count: "5",
1066 | userCount: 2,
1067 | permalink: "https://sentry-mcp-evals.sentry.io/issues/123456/",
1068 | project: {
1069 | id: "4509062593708032",
1070 | name: "TEST-PROJECT",
1071 | slug: "test-project",
1072 | platform: "javascript",
1073 | },
1074 | status: "unresolved",
1075 | culprit: "app.js in processData",
1076 | type: "error",
1077 | platform: "javascript",
1078 | });
1079 | },
1080 | ),
1081 | http.get(
1082 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CONTEXT-001/events/latest/",
1083 | () => {
1084 | return HttpResponse.json(eventWithContext);
1085 | },
1086 | ),
1087 | );
1088 |
1089 | const result = await getIssueDetails.handler(
1090 | {
1091 | organizationSlug: "sentry-mcp-evals",
1092 | issueId: "CONTEXT-001",
1093 | eventId: undefined,
1094 | issueUrl: undefined,
1095 | regionUrl: undefined,
1096 | },
1097 | {
1098 | constraints: {
1099 | organizationSlug: null,
1100 | },
1101 | accessToken: "access-token",
1102 | userId: "1",
1103 | },
1104 | );
1105 |
1106 | // Verify the context (extra) data is displayed
1107 | expect(result).toContain("### Extra Data");
1108 | expect(result).toContain("Additional data attached to this event");
1109 | expect(result).toContain('**custom_field**: "custom_value"');
1110 | expect(result).toContain('**user_action**: "submit_form"');
1111 | expect(result).toContain("**session_data**:");
1112 | expect(result).toContain('"session_id": "sess_12345"');
1113 | expect(result).toContain('"user_id": "user_67890"');
1114 | expect(result).toContain('**environment_info**: "production"');
1115 | // Verify contexts are still displayed
1116 | expect(result).toContain("### Additional Context");
1117 | });
1118 | });
1119 |
```
--------------------------------------------------------------------------------
/packages/mcp-server-mocks/src/fixtures/autofix-state.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "autofix": {
3 | "run_id": 21831,
4 | "request": {
5 | "project_id": 4505138086019073
6 | },
7 | "status": "COMPLETED",
8 | "updated_at": "2025-04-09T22:39:50.778146",
9 | "steps": [
10 | {
11 | "active_comment_thread": null,
12 | "agent_comment_thread": null,
13 | "completedMessage": null,
14 | "id": "5c3238ea-4c3a-4c02-a94b-92a3ca25c946",
15 | "index": 0,
16 | "initial_memory_length": 1,
17 | "insights": [
18 | {
19 | "change_diff": null,
20 | "generated_at_memory_index": 0,
21 | "insight": "The `bottleById` query fails because the input ID (3216) doesn't exist in the database.\n",
22 | "justification": "The exception details show that the `input` value at the time of the `TRPCError` in `bottleById.ts` was 3216, and the query likely failed because a bottle with ID 3216 was not found in the database.\n\n```\nVariable values at the time of the exception::\n{\n \"input\": 3216\n}\n```\n",
23 | "type": "insight"
24 | },
25 | {
26 | "change_diff": null,
27 | "generated_at_memory_index": 22,
28 | "insight": "However, the request also includes a different ID (16720) for `bottlePriceList`.\n",
29 | "justification": "The root cause is likely a mismatch of input IDs within the batched TRPC request, where `bottlePriceList` expects bottle ID 16720, but `bottleById` receives a different ID (3216) leading to the \"Bottle not found\" error.\n\n```\nGET http://api.peated.com/trpc/bottlePriceList,bottleById\n```\n\n```json\n{\n \"input\": 3216\n}\n```\n\n```\nTRPCError: Bottle not found. (occurred in: GET /trpc/bottlePriceList,bottleById)\n```\n",
30 | "type": "insight"
31 | },
32 | {
33 | "change_diff": null,
34 | "generated_at_memory_index": 22,
35 | "insight": "This suggests a data consistency issue or incorrect client-side request.\n",
36 | "justification": "The `TRPCError` originates from `bottleById.ts` with the input value being `3216`, indicating the procedure failed to find a bottle with that specific ID in the database.\n\n```\n <anonymous> in file /app/apps/server/src/trpc/routes/bottleById.ts [Line 33, column 13] (In app)\n .select({\n ...getTableColumns(bottles),\n })\n .from(bottleTombstones)\n .innerJoin(bottles, eq(bottleTombstones.newBottleId, bottles.id))\n .where(eq(bottleTombstones.bottleId, input));\n if (!bottle) {\n throw new TRPCError({ <-- SUSPECT LINE\n message: \"Bottle not found.\",\n code: \"NOT_FOUND\",\n });\n }\n }\n\n const createdBy = await db.query.users.findFirst({\n---\nVariable values at the time of the exception::\n{\n \"input\": 3216\n}\n```\n",
37 | "type": "insight"
38 | }
39 | ],
40 | "key": "root_cause_analysis_processing",
41 | "output_confidence_score": null,
42 | "output_stream": "Let me summarize what I know about this issue:\n\n1. The error occurs in the `bottleById.ts` endpoint, with the error \"Bottle not found.\"\n2. The request is to `/trpc/bottlePriceList,bottleById` - which suggests that two TRPC procedures are being called in the same request.\n3. According to the error trace, the input ID is 16720, but the exception details show 3216 as the input value.\n4. The code in `bottleById.ts` tries to look up a bottle in two places:\n - First in the main `bottles` table\n - Then in the `bottleTombstones` table (which handles redirects for bottles that have been merged/moved)\n\nThe most significant clue is the discrepancy between the ID mentioned in the error (16720) and the ID in the exception details (3216). This suggests there's something fishy going on with how the IDs are being processed.\n\nGiven that the URL shows `bottlePriceList,bottleById`, this suggests a batched TRPC request where two procedures are being called simultaneously. The batched nature of TRPC calls might be relevant here.\n\nLet's think about potential root causes:\n\n1. **Data problem**: Neither bottle ID 16720 nor 3216 exists in the database.\n2. **Race condition**: The bottle might have been deleted between the two calls.\n3. **TRPC batching issue**: There might be a problem with how the batched request is handling the different IDs for the two procedures.\n4. **Input switching**: The ID is somehow being switched between the procedures.\n\nBased on the evidence, I think the most likely explanation is related to the TRPC batching. When the client makes a request to `/trpc/bottlePriceList,bottleById`, it's calling two procedures:\n- `bottlePriceList` with one ID (maybe 16720)\n- `bottleById` with another ID (3216)\n\nThe error is occurring in the `bottleById` procedure with ID 3216, but the overall request is being reported with ID 16720 (which might be from the `bottlePriceList` part of the batch).\n\nSince the exception is specifically coming from `bottleById.ts` with input 3216, the root cause is that there is no bottle with ID 3216 in the database (neither in `bottles` nor in `bottleTombstones`).\n\nThe confusing part is that the error message mentions 16720, which might be:\n1. A UI issue where the ID displayed doesn't match what's being requested\n2. A client-side issue where the wrong ID is being provided to `bottleById`\n3. A batching issue where the ID from one procedure is being displayed in the error for another\n\nGiven this analysis, the true root cause is: **The `bottleById` procedure is being called with ID 3216, but no bottle with that ID exists in the database.**\n\nThis is definitely a data consistency issue - either the bottle with ID 3216 was deleted without creating a tombstone, or the client is requesting a non-existent ID.# Root Cause Analysis: TRPCError: Bottle not found\n\nAfter examining all the available evidence, I can identify the true root cause of this issue:\n\n## Root Cause\n\n**The issue is related to a TRPC batch request inconsistency between the requested bottle ID and what's being processed.**\n\nThis conclusion is based on several key observations:\n\n1. The request is made to `/trpc/bottlePriceList,bottleById` - this is a batched TRPC request calling two procedures simultaneously.\n\n2. **Critical discrepancy**: The error message references bottle ID 16720, but the exception details show the procedure was actually processing ID 3216.\n\n3. Both procedures (`bottleById` and `bottlePriceList`) contain similar \"Bottle not found\" error checks, but the one that's triggering is in `bottleById.ts`.\n\n4. The `bottleById` procedure is attempting to find bottle ID 3216, not finding it in the main bottles table, then failing to find it in the tombstones table as well.\n\n## Detailed Explanation\n\nWhat's happening is:\n\n1. The client makes a batched request to both `bottlePriceList` and `bottleById` endpoints.\n\n2. Based on the URL and error, it appears that:\n - The `bottlePriceList` procedure might be receiving ID 16720\n - The `bottleById` procedure is receiving ID 3216\n\n3. When `bottleById` tries to process ID 3216, it can't find this bottle in the database or in tombstones, so it throws the \"Bottle not found\" error.\n\n4. The error reporting is showing the ID from the overall request (16720) while the actual failure is happening with a different ID (3216).\n\n## Why This Is Happening\n\nThis is likely either:\n\n1. **Client-side issue**: The client is passing the wrong ID to `bottleById` in the batched request\n2. **Data integrity issue**: Bottle ID 3216 may have been deleted without creating a tombstone entry\n3. **Parameter mixing**: The batched TRPC request might be incorrectly mapping parameters between the two procedures\n\nThe fact that two different IDs are involved (16720 and 3216) strongly suggests this is not a simple case of a missing bottle, but rather a data inconsistency or parameter handling issue in the batched request.\n\nTo fix this issue, you would need to:\n1. Check if bottle ID 3216 should exist in the database\n2. Verify how the client is forming the batched request and what IDs it's sending to each procedure\n3. Consider adding more detailed logging that shows the exact inputs to each procedure in batched requests",
43 | "proceed_confidence_score": null,
44 | "progress": [
45 | {
46 | "data": null,
47 | "message": "Figuring out the root cause...",
48 | "timestamp": "2025-04-09T22:35:43.590244",
49 | "type": "INFO"
50 | },
51 | {
52 | "data": null,
53 | "message": "Looking at `apps/server/src/trpc/routes/bottleById.ts` in `dcramer/peated`...",
54 | "timestamp": "2025-04-09T22:35:52.808244",
55 | "type": "INFO"
56 | },
57 | {
58 | "data": null,
59 | "message": "Looking at `apps/server/src/db/schema.ts` in `dcramer/peated`...",
60 | "timestamp": "2025-04-09T22:36:14.424666",
61 | "type": "INFO"
62 | },
63 | {
64 | "data": null,
65 | "message": "Searching files with `find apps/server/src/db -type f -name \"*.ts\"`...",
66 | "timestamp": "2025-04-09T22:36:16.500156",
67 | "type": "INFO"
68 | },
69 | {
70 | "data": null,
71 | "message": "Looking at `apps/server/src/db/schema/index.ts` in `dcramer/peated`...",
72 | "timestamp": "2025-04-09T22:36:18.687233",
73 | "type": "INFO"
74 | },
75 | {
76 | "data": null,
77 | "message": "Looking at `apps/server/src/db/schema/bottles.ts` in `dcramer/peated`...",
78 | "timestamp": "2025-04-09T22:36:21.205568",
79 | "type": "INFO"
80 | },
81 | {
82 | "data": null,
83 | "message": "Looking at `apps/server/src/db/index.ts` in `dcramer/peated`...",
84 | "timestamp": "2025-04-09T22:36:24.013172",
85 | "type": "INFO"
86 | },
87 | {
88 | "data": null,
89 | "message": "Looking at `apps/server/src/db/schema/storePrices.ts` in `dcramer/peated`...",
90 | "timestamp": "2025-04-09T22:36:27.236327",
91 | "type": "INFO"
92 | },
93 | {
94 | "data": null,
95 | "message": "Looking at `apps/server/src/db/schema/stores.ts` in `dcramer/peated`...",
96 | "timestamp": "2025-04-09T22:36:29.450102",
97 | "type": "INFO"
98 | },
99 | {
100 | "data": null,
101 | "message": "Looking at `apps/server/src/db/schema/tastings.ts` in `dcramer/peated`...",
102 | "timestamp": "2025-04-09T22:36:31.593538",
103 | "type": "INFO"
104 | },
105 | {
106 | "data": null,
107 | "message": "Searching for \"Get information about the bottle price list controller or route that might be calling bottleById.ts\"...",
108 | "timestamp": "2025-04-09T22:36:34.373043",
109 | "type": "INFO"
110 | },
111 | {
112 | "data": null,
113 | "message": "Simulating profound thought...",
114 | "timestamp": "2025-04-09T22:36:56.212382",
115 | "type": "INFO"
116 | },
117 | {
118 | "data": null,
119 | "message": "Arranging data in a way that looks intentional...",
120 | "timestamp": "2025-04-09T22:37:33.353804",
121 | "type": "INFO"
122 | }
123 | ],
124 | "queued_user_messages": [],
125 | "status": "COMPLETED",
126 | "title": "Analyzing the Issue",
127 | "type": "default"
128 | },
129 | {
130 | "active_comment_thread": null,
131 | "agent_comment_thread": null,
132 | "causes": [
133 | {
134 | "description": "Batched TRPC request incorrectly passed bottle ID 3216 to `bottleById`, instead of 16720, resulting in a \"Bottle not found\" error.",
135 | "id": 0,
136 | "root_cause_reproduction": [
137 | {
138 | "code_snippet_and_analysis": "This is the entry point where the client requests data from two different procedures in a single HTTP request. The server needs to correctly route and process the parameters for each procedure.",
139 | "is_most_important_event": false,
140 | "relevant_code_file": null,
141 | "timeline_item_type": "human_action",
142 | "title": "The client initiates a batched TRPC request to the `/trpc/bottlePriceList,bottleById` endpoint, intending to fetch both the price list and details for a specific bottle."
143 | },
144 | {
145 | "code_snippet_and_analysis": "```typescript\n// apps/server/src/trpc/routes/bottlePriceList.ts\n.input(z.object({ bottle: z.number(), onlyValid: z.boolean().optional() }))\n.query(async function ({ input, ctx }) {\n const [bottle] = await db.select().from(bottles).where(eq(bottles.id, input.bottle));\n if (!bottle) { ... }\n```\nThis procedure expects a 'bottle' parameter in the input, which is used to query the database.",
146 | "is_most_important_event": false,
147 | "relevant_code_file": {
148 | "file_path": "apps/server/src/trpc/routes/bottlePriceList.ts",
149 | "repo_name": "dcramer/peated"
150 | },
151 | "timeline_item_type": "internal_code",
152 | "title": "The TRPC server receives the batched request and begins processing the `bottlePriceList` procedure, intending to fetch the price list for bottle ID 16720."
153 | },
154 | {
155 | "code_snippet_and_analysis": "```typescript\n// apps/server/src/trpc/routes/bottleById.ts\nexport default publicProcedure.input(z.number()).query(async function ({ input, ctx }) {\n let [bottle] = await db.select().from(bottles).where(eq(bottles.id, input));\n if (!bottle) { ... }\n```\nThis procedure expects a numerical ID as input to find the bottle.",
156 | "is_most_important_event": true,
157 | "relevant_code_file": {
158 | "file_path": "apps/server/src/trpc/routes/bottleById.ts",
159 | "repo_name": "dcramer/peated"
160 | },
161 | "timeline_item_type": "internal_code",
162 | "title": "The TRPC server also processes the `bottleById` procedure, but due to a parameter mapping issue or client-side error, it receives bottle ID 3216 as input instead of 16720."
163 | },
164 | {
165 | "code_snippet_and_analysis": "The database query returns no results because bottle ID 3216 is not present in the `bottles` table.",
166 | "is_most_important_event": false,
167 | "relevant_code_file": {
168 | "file_path": "apps/server/src/trpc/routes/bottleById.ts",
169 | "repo_name": "dcramer/peated"
170 | },
171 | "timeline_item_type": "external_system",
172 | "title": "The `bottleById` procedure queries the `bottles` table for a bottle with ID 3216, but no such bottle exists."
173 | },
174 | {
175 | "code_snippet_and_analysis": "The query to `bottleTombstones` also returns no results, indicating that bottle ID 3216 has not been redirected.",
176 | "is_most_important_event": false,
177 | "relevant_code_file": {
178 | "file_path": "apps/server/src/trpc/routes/bottleById.ts",
179 | "repo_name": "dcramer/peated"
180 | },
181 | "timeline_item_type": "external_system",
182 | "title": "The `bottleById` procedure then checks the `bottleTombstones` table to see if bottle ID 3216 has been tombstoned (redirected to a new ID), but no such tombstone exists."
183 | },
184 | {
185 | "code_snippet_and_analysis": "```typescript\n// apps/server/src/trpc/routes/bottleById.ts\nif (!bottle) {\n throw new TRPCError({ message: \"Bottle not found.\", code: \"NOT_FOUND\" });\n}\n```\nThis is where the error is thrown, indicating that the bottle could not be found.",
186 | "is_most_important_event": false,
187 | "relevant_code_file": {
188 | "file_path": "apps/server/src/trpc/routes/bottleById.ts",
189 | "repo_name": "dcramer/peated"
190 | },
191 | "timeline_item_type": "internal_code",
192 | "title": "Since the `bottleById` procedure cannot find a bottle with ID 3216 in either the `bottles` or `bottleTombstones` tables, it throws a `TRPCError` with the message \"Bottle not found.\""
193 | }
194 | ]
195 | }
196 | ],
197 | "completedMessage": null,
198 | "id": "39166714-b14d-4fa0-a122-3ac241f7b46a",
199 | "index": 1,
200 | "key": "root_cause_analysis",
201 | "output_confidence_score": 0.95,
202 | "output_stream": null,
203 | "proceed_confidence_score": 0.9,
204 | "progress": [
205 | {
206 | "data": null,
207 | "message": "Here is Seer's proposed root cause.",
208 | "timestamp": "2025-04-09T22:37:40.934397",
209 | "type": "INFO"
210 | }
211 | ],
212 | "queued_user_messages": [],
213 | "selection": { "cause_id": 0, "instruction": null },
214 | "status": "COMPLETED",
215 | "termination_reason": null,
216 | "title": "Root Cause Analysis",
217 | "type": "root_cause_analysis"
218 | },
219 | {
220 | "active_comment_thread": null,
221 | "agent_comment_thread": null,
222 | "completedMessage": null,
223 | "id": "649acf61-9922-4ac4-925d-9b73302ac8e9",
224 | "index": 2,
225 | "initial_memory_length": 5,
226 | "insights": [
227 | {
228 | "change_diff": null,
229 | "generated_at_memory_index": 4,
230 | "insight": "The discrepancy between inputs suggests a potential issue with batch request handling.\n",
231 | "justification": "The `TRPCError` occurred in `bottleById.ts` with an input of `3216`, while the request was initiated with a bottle ID of `16720`, indicating a mismatch in the bottle ID being processed.\n\n```\nVariable values at the time of the exception::\n{\n \"input\": 3216\n}\n```\n\n```\nGET http://api.peated.com/trpc/bottlePriceList,bottleById\n```\n",
232 | "type": "insight"
233 | },
234 | {
235 | "change_diff": null,
236 | "generated_at_memory_index": 44,
237 | "insight": "The client-side batch request construction is the most likely source of error.\n",
238 | "justification": "The discrepancy between the bottle ID in the error message (16720) and the ID being processed (3216) strongly suggests a client-side parameter mapping issue in the batched TRPC request.\n\nTo fix this, I propose the following steps:\n\n1. **Inspect the Client-Side Code:** Identify the code responsible for constructing the TRPC batch request to `/trpc/bottlePriceList,bottleById`.\n\n2. **Verify Parameter Mapping:** Ensure that both `bottlePriceList` and `bottleById` are receiving the correct bottle ID (which should be 16720 in this case).\n\n3. **Correct Parameter Assignment:** If the parameters are being incorrectly assigned, modify the client-side code to ensure that both procedures receive the same, correct bottle ID.\n\n4. **Add Logging (Client-Side):** Add temporary logging to the client-side code to confirm the parameters being sent to each procedure in the batch request. This will help verify the fix.\n\n ```typescript\n // Example logging (add to the client-side code where the batch request is created)\n console.log(\"bottlePriceList input:\", { bottle: 16720 }); // Replace 16720 with the actual ID\n console.log(\"bottleById input:\", { bottle: 16720 }); // Replace 16720 with the actual ID\n ```\n\n5. **Test the Fix:** After applying the fix, test the affected functionality to ensure that the \"Bottle not found\" error is resolved and that both procedures are working correctly.\n\n6. **Remove Logging:** Once the fix is verified, remove the temporary logging.\n\nThis approach directly addresses the most likely cause of the issue and provides a clear path to resolution.\n",
239 | "type": "insight"
240 | },
241 | {
242 | "change_diff": null,
243 | "generated_at_memory_index": 44,
244 | "insight": "Ensuring consistent bottle IDs will likely resolve this inconsistency.\n",
245 | "justification": "The error message references bottle ID 16720, but the exception details show the procedure was actually processing ID 3216, indicating a parameter mismatch in the batch request.\n\nTo fix this, I recommend the following steps:\n\n1. **Client-Side Investigation**:\n\n * Examine the client-side code where the TRPC batch request is constructed.\n * Verify that the same `bottleId` parameter is being passed to both `bottlePriceList` and `bottleById` procedures.\n * If the IDs are being passed correctly, inspect the network request payload to confirm the correct structure of the batched request.\n\n ```typescript\n // Example (Hypothetical) Client-Side Code\n const bottleId = 16720; // Example bottle ID\n\n // Ensure both procedures receive the same bottleId\n const [priceList, bottleDetails] = await trpc.batch(() => [\n trpc.bottlePriceList.fetch({ bottle: bottleId }),\n trpc.bottleById.fetch(bottleId),\n ]);\n ```\n\n2. **Server-Side Logging (Temporary)**:\n\n * Add temporary logging to both `bottlePriceList` and `bottleById` procedures to log the received `input` value.\n * This will help confirm whether the server is receiving the correct IDs from the client.\n * **Important**: Remove these logs after debugging to avoid unnecessary overhead.\n\n ```typescript\n // apps/server/src/trpc/routes/bottlePriceList.ts\n export default publicProcedure\n .input(\n z.object({\n bottle: z.number(),\n onlyValid: z.boolean().optional(),\n }),\n )\n .query(async function ({ input, ctx }) {\n console.log(\"bottlePriceList input:\", input); // Add this line\n // ... rest of the code\n });\n\n // apps/server/src/trpc/routes/bottleById.ts\n export default publicProcedure.input(z.number()).query(async function ({\n input,\n ctx,\n }) {\n console.log(\"bottleById input:\", input); // Add this line\n // ... rest of the code\n });\n ```\n\n3. **TRPC Batch Request Configuration**:\n\n * Review the TRPC batch link configuration on the client-side.\n * Ensure that the batching logic is correctly mapping parameters to the corresponding procedures.\n * If using a custom batching implementation, verify its correctness.\n\n4. **Data Integrity Check**:\n\n * If the client-side code appears correct, investigate whether bottle ID 3216 should exist in the database.\n * Check the `bottles` table and `bottleTombstones` table for any entries related to bottle ID 3216.\n * If the bottle should exist but is missing, investigate potential data deletion or migration issues.\n\n5. **Tombstone Logic**:\n\n * Double-check the logic for creating and using tombstones.\n * Ensure that when a bottle is deleted, a tombstone entry is created correctly, pointing to the new bottle (if any).\n\n6. **Error Handling**:\n\n * While this isn't the primary fix, consider improving the error message in `bottleById.ts` to include more context.\n * Include the original requested bottle ID (if available) in the error message to aid debugging.\n\n7. **Client-Side Retries**:\n\n * Implement a retry mechanism on the client-side for TRPC requests.\n * If a \"Bottle not found\" error occurs, retry the request a few times before giving up. This can help mitigate transient issues.\n\nBy following these steps, you should be able to identify the root cause of the parameter mismatch and implement a fix that ensures consistent bottle IDs are passed to both TRPC procedures in the batch request.\n",
246 | "type": "insight"
247 | }
248 | ],
249 | "key": "solution_processing",
250 | "output_confidence_score": null,
251 | "output_stream": "Based on all the information I've gathered, I'm going to develop a clear fix for this issue. Let's summarize the core problem first:\n\n1. We have a TRPC batch request that calls both `bottlePriceList` and `bottleById` procedures in a single HTTP request.\n2. The error shows a discrepancy between the bottle ID in the error message (16720) and the ID being processed when the error occurred (3216).\n3. The `bottleById` procedure fails because it's trying to find bottle ID 3216, which doesn't exist in the database or in tombstones.\n\nThe most likely issue is a parameter mismatch in the batch TRPC request. When examining the client code and the trace, it seems like the client-side code is sending different bottle IDs to the two procedures incorrectly.\n\nLet's analyze the most probable causes and their solutions:\n\n1. **Client-side parameter mismatch**: The client might be configuring the batch request incorrectly, sending bottle ID 16720 to `bottlePriceList` and bottle ID 3216 to `bottleById`.\n\n2. **Server-side parameter confusion**: There could be some issue with how parameters are being mapped in batched TRPC requests on the server side.\n\n3. **Application-level logic issue**: The application might be assuming that both procedures should operate on the same bottle ID, but this assumption is being violated during batch requests.\n\nFrom the code snippets I've seen, the most likely issue is in the client-side code where the batch request is being constructed. The solution should ensure consistency in parameters across the batch request.\n\nSince we can see from traces that the client is attempting to fetch bottle details, and the frontend code shows that the `bottlePriceList` and `bottleById` are supposed to operate on the same bottle ID, I'm going to recommend a solution that ensures parameter consistency.\n\nThe most actionable and effective fix would be to ensure that both procedures receive the same bottle ID in the batch request. Let's outline that solution in detail:# Solution to Fix the \"Bottle not found\" TRPC Error\n\n## Root Cause\nThe issue stems from a parameter mismatch in TRPC batch requests. When a request is made to `/trpc/bottlePriceList,bottleById`, the procedures are receiving different bottle IDs:\n- `bottlePriceList` receives ID **16720**\n- `bottleById` receives ID **3216**\n\nThe error occurs because bottle ID **3216** doesn't exist in the database.\n\n## Solution Steps\n\n### 1. Fix the Client-Side Batch Request Configuration\n\nThe most direct solution is to ensure parameter consistency in the batch request. Based on the web app's bottle page implementation, both procedures should receive the same bottle ID.\n\n```typescript\n// In apps/web/src/app/(default)/bottles/[bottleId]/(tabs)/page.tsx\nexport default async function BottleDetails({\n params: { bottleId },\n}: {\n params: { bottleId: string };\n}) {\n const trpcClient = await getTrpcClient();\n \n // PROBLEM: These are being called separately, potentially with different batching behavior\n const bottle = await trpcClient.bottleById.fetch(Number(bottleId));\n \n // ...rest of the function\n}\n\n// In apps/web/src/app/(default)/bottles/[bottleId]/(tabs)/prices/page.tsx\nexport default async function BottlePrices({\n params: { bottleId },\n}: {\n params: { bottleId: string };\n}) {\n const trpcClient = await getTrpcClient();\n const priceList = await trpcClient.bottlePriceList.fetch({\n bottle: Number(bottleId),\n });\n \n // ...rest of the function\n}\n```\n\n### 2. Implement Proper Error Handling for Missing Bottles\n\nAdd graceful handling when a bottle isn't found, especially in the context of batch requests:\n\n```typescript\n// In apps/server/src/trpc/routes/bottleById.ts\nexport default publicProcedure.input(z.number()).query(async function ({\n input,\n ctx,\n}) {\n try {\n let [bottle] = await db.select().from(bottles).where(eq(bottles.id, input));\n\n if (!bottle) {\n // check for a tombstone\n [bottle] = await db\n .select({\n ...getTableColumns(bottles),\n })\n .from(bottleTombstones)\n .innerJoin(bottles, eq(bottleTombstones.newBottleId, bottles.id))\n .where(eq(bottleTombstones.bottleId, input));\n if (!bottle) {\n throw new TRPCError({\n message: `Bottle not found with ID: ${input}`, // Include ID in error for better debugging\n code: \"NOT_FOUND\",\n });\n }\n }\n \n // Rest of the function...\n } catch (error) {\n // Log the error with context\n console.error(`Error fetching bottle ID ${input}:`, error);\n throw error;\n }\n});\n```\n\n### 3. Update the Batch Request Implementation to Ensure Parameter Consistency\n\nThe most thorough solution is to modify how pages that need both bottle details and price data fetch their information:\n\n```typescript\n// In a shared utility function or component\nexport async function getBottleWithPrices(bottleId: number) {\n const trpcClient = await getTrpcClient();\n \n // Use Promise.all to ensure both requests are part of the same batch\n // and receive the same parameters\n const [bottle, priceList] = await Promise.all([\n trpcClient.bottleById.fetch(bottleId),\n trpcClient.bottlePriceList.fetch({ bottle: bottleId }),\n ]);\n \n return { bottle, priceList };\n}\n\n// Then in the page components:\nconst { bottle, priceList } = await getBottleWithPrices(Number(bottleId));\n```\n\n### 4. Add Debugging to TRPC Batch Requests\n\nTo prevent similar issues in the future, add better logging for TRPC batch requests:\n\n```typescript\n// In apps/server/src/trpc/index.ts\nexport const publicProcedure = t.procedure.use(sentryMiddleware).use(\n t.middleware(async ({ ctx, next, path, input }) => {\n // Log the procedure name and input for debugging\n console.log(`TRPC procedure: ${path}, input:`, input);\n \n return next({\n ctx,\n });\n })\n);\n```\n\n## Implementation Priority\n\n1. **First implement solution #3** - Update how batch requests are made to ensure parameter consistency\n2. **Then implement solution #2** - Improve error messaging and handling\n3. **Consider solution #4** for monitoring and preventing future issues\n\nThis approach addresses the immediate problem while also improving the robustness of the application against similar issues in the future.",
252 | "proceed_confidence_score": null,
253 | "progress": [
254 | {
255 | "data": null,
256 | "message": "Figuring out a solution...",
257 | "timestamp": "2025-04-09T22:37:41.090125",
258 | "type": "INFO"
259 | },
260 | {
261 | "data": null,
262 | "message": "Looking at `apps/server/src/db/schema.ts` in `dcramer/peated`...",
263 | "timestamp": "2025-04-09T22:37:58.226774",
264 | "type": "INFO"
265 | },
266 | {
267 | "data": null,
268 | "message": "Searching files with `find . -name \"schema.ts\" -o -name \"*schema*.ts\"`...",
269 | "timestamp": "2025-04-09T22:38:06.650338",
270 | "type": "INFO"
271 | },
272 | {
273 | "data": null,
274 | "message": "Searching for \"database schema definition for bottles and bottleTombstones\"...",
275 | "timestamp": "2025-04-09T22:38:09.204410",
276 | "type": "INFO"
277 | },
278 | {
279 | "data": null,
280 | "message": "Looking at `apps/server/src/db/schema/storePrices.ts` in `dcramer/peated`...",
281 | "timestamp": "2025-04-09T22:38:12.727565",
282 | "type": "INFO"
283 | },
284 | {
285 | "data": null,
286 | "message": "Searching for \"database schema for storePrices and externalSites\"...",
287 | "timestamp": "2025-04-09T22:38:14.892955",
288 | "type": "INFO"
289 | },
290 | {
291 | "data": null,
292 | "message": "Searching for \"schema definition for storePrices table\"...",
293 | "timestamp": "2025-04-09T22:38:18.078667",
294 | "type": "INFO"
295 | },
296 | {
297 | "data": null,
298 | "message": "Grepping codebase with `grep -r \"public_trpc\" --include=\"*.ts\" --include=\"*.tsx\"`...",
299 | "timestamp": "2025-04-09T22:38:22.072610",
300 | "type": "INFO"
301 | },
302 | {
303 | "data": null,
304 | "message": "Grepping codebase with `grep -r \"createTRPCRouter\" --include=\"*.ts\" --include=\"*.tsx\"`...",
305 | "timestamp": "2025-04-09T22:38:24.640654",
306 | "type": "INFO"
307 | },
308 | {
309 | "data": null,
310 | "message": "Searching for \"TRPC router definition or main TRPC setup\"...",
311 | "timestamp": "2025-04-09T22:38:27.296050",
312 | "type": "INFO"
313 | },
314 | {
315 | "data": null,
316 | "message": "Looking at `apps/server/src/trpc/index.ts` in `dcramer/peated`...",
317 | "timestamp": "2025-04-09T22:38:31.342500",
318 | "type": "INFO"
319 | },
320 | {
321 | "data": null,
322 | "message": "Grepping codebase with `grep -r \"SELECT.*FROM bottle\" --include=\"*.ts\" --include=\"*.tsx\"`...",
323 | "timestamp": "2025-04-09T22:38:33.896598",
324 | "type": "INFO"
325 | },
326 | {
327 | "data": null,
328 | "message": "Looking at `apps/web/src/routes/[regionSlug]/[...bottleSlug].tsx` in `dcramer/peated`...",
329 | "timestamp": "2025-04-09T22:38:36.691936",
330 | "type": "INFO"
331 | },
332 | {
333 | "data": null,
334 | "message": "Searching files with `find . -path \"*bottle*\" -name \"*.tsx\"`...",
335 | "timestamp": "2025-04-09T22:38:39.431421",
336 | "type": "INFO"
337 | },
338 | {
339 | "data": null,
340 | "message": "Looking at `apps/web/src/app/(default)/bottles/[bottleId]/(tabs)/prices/page.tsx` in `dcramer/peated`...",
341 | "timestamp": "2025-04-09T22:38:43.030734",
342 | "type": "INFO"
343 | },
344 | {
345 | "data": null,
346 | "message": "Looking at `apps/web/src/app/(default)/bottles/[bottleId]/(tabs)/page.tsx` in `dcramer/peated`...",
347 | "timestamp": "2025-04-09T22:38:45.535641",
348 | "type": "INFO"
349 | },
350 | {
351 | "data": null,
352 | "message": "Looking at `apps/web/src/lib/trpc/client.server.ts` in `dcramer/peated`...",
353 | "timestamp": "2025-04-09T22:38:48.766893",
354 | "type": "INFO"
355 | },
356 | {
357 | "data": null,
358 | "message": "Looking at `apps/server/src/trpc/links.ts` in `dcramer/peated`...",
359 | "timestamp": "2025-04-09T22:38:51.786534",
360 | "type": "INFO"
361 | },
362 | {
363 | "data": null,
364 | "message": "Looking at `apps/server/src/trpc/context.ts` in `dcramer/peated`...",
365 | "timestamp": "2025-04-09T22:38:54.281514",
366 | "type": "INFO"
367 | },
368 | {
369 | "data": null,
370 | "message": "Studying spans under `http.server - GET /trpc/bottlePriceList,bottleById`...",
371 | "timestamp": "2025-04-09T22:39:00.325683",
372 | "type": "INFO"
373 | },
374 | {
375 | "data": null,
376 | "message": "Being artificially intelligent...",
377 | "timestamp": "2025-04-09T22:39:19.353863",
378 | "type": "INFO"
379 | },
380 | {
381 | "data": null,
382 | "message": "Formatting for human consumption...",
383 | "timestamp": "2025-04-09T22:39:47.228661",
384 | "type": "INFO"
385 | }
386 | ],
387 | "queued_user_messages": [],
388 | "status": "COMPLETED",
389 | "title": "Planning Solution",
390 | "type": "default"
391 | },
392 | {
393 | "active_comment_thread": null,
394 | "agent_comment_thread": null,
395 | "completedMessage": null,
396 | "custom_solution": null,
397 | "description": "Consolidate bottle and price data fetching into a single batched TRPC request using `Promise.all` to ensure ID consistency.",
398 | "id": "87ba0ee3-af4e-458e-b3a9-6fd5f9d4a471",
399 | "index": 3,
400 | "key": "solution",
401 | "output_confidence_score": null,
402 | "output_stream": null,
403 | "proceed_confidence_score": null,
404 | "progress": [
405 | {
406 | "data": null,
407 | "message": "Here is Seer's proposed solution.",
408 | "timestamp": "2025-04-09T22:39:50.191715",
409 | "type": "INFO"
410 | }
411 | ],
412 | "queued_user_messages": [],
413 | "selected_mode": null,
414 | "solution": [
415 | {
416 | "code_snippet_and_analysis": "```typescript\n// In a shared utility function or component\nexport async function getBottleWithPrices(bottleId: number) {\n const trpcClient = await getTrpcClient();\n \n // Use Promise.all to ensure both requests are part of the same batch\n // and receive the same parameters\n const [bottle, priceList] = await Promise.all([\n trpcClient.bottleById.fetch(bottleId),\n trpcClient.bottlePriceList.fetch({ bottle: bottleId }),\n ]);\n \n return { bottle, priceList };\n}\n```\nThis code creates a function that uses `Promise.all` to fetch both bottle details and price data concurrently. This ensures that both TRPC procedures are part of the same batch and receive the same `bottleId`.",
417 | "is_active": true,
418 | "is_most_important_event": true,
419 | "relevant_code_file": null,
420 | "timeline_item_type": "internal_code",
421 | "title": "Create a shared utility function to fetch bottle details and price data together."
422 | },
423 | {
424 | "code_snippet_and_analysis": "```typescript\n// Then in the page components:\nconst { bottle, priceList } = await getBottleWithPrices(Number(bottleId));\n```\nThis code replaces the separate calls to `bottleById` and `bottlePriceList` with a single call to the `getBottleWithPrices` function, ensuring that both components receive data for the same bottle.",
425 | "is_active": true,
426 | "is_most_important_event": false,
427 | "relevant_code_file": null,
428 | "timeline_item_type": "internal_code",
429 | "title": "Modify the page components to use the shared utility function."
430 | },
431 | {
432 | "code_snippet_and_analysis": null,
433 | "is_active": false,
434 | "is_most_important_event": false,
435 | "relevant_code_file": null,
436 | "timeline_item_type": "repro_test",
437 | "title": "Add a unit test that reproduces the issue."
438 | }
439 | ],
440 | "solution_selected": false,
441 | "status": "COMPLETED",
442 | "title": "Solution",
443 | "type": "solution"
444 | }
445 | ]
446 | }
447 | }
448 |
```
--------------------------------------------------------------------------------
/packages/mcp-server-mocks/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * MSW-based Mock Server for Sentry MCP Development and Testing.
3 | *
4 | * Provides comprehensive mock responses for all Sentry API endpoints used by the
5 | * MCP server. Built with MSW (Mock Service Worker) for realistic HTTP interception
6 | * and response handling during development and testing.
7 | *
8 | * **Usage in Tests:**
9 | * ```typescript
10 | * import { mswServer } from "@sentry/mcp-server-mocks";
11 | *
12 | * beforeAll(() => mswServer.listen());
13 | * afterEach(() => mswServer.resetHandlers());
14 | * afterAll(() => mswServer.close());
15 | * ```
16 | *
17 | * **Usage in Development:**
18 | * ```typescript
19 | * // Start mock server for local development
20 | * mswServer.listen();
21 | * // Now all Sentry API calls will be intercepted
22 | * ```
23 | */
24 | import { setupServer } from "msw/node";
25 | import { http, HttpResponse } from "msw";
26 |
27 | import autofixStateFixture from "./fixtures/autofix-state.json" with {
28 | type: "json",
29 | };
30 | import issueFixture from "./fixtures/issue.json" with { type: "json" };
31 | import eventsFixture from "./fixtures/event.json" with { type: "json" };
32 | import performanceEventFixture from "./fixtures/performance-event.json" with {
33 | type: "json",
34 | };
35 | import eventAttachmentsFixture from "./fixtures/event-attachments.json" with {
36 | type: "json",
37 | };
38 | import tagsFixture from "./fixtures/tags.json" with { type: "json" };
39 | import projectFixture from "./fixtures/project.json" with { type: "json" };
40 | import teamFixture from "./fixtures/team.json" with { type: "json" };
41 | import traceItemsAttributesFixture from "./fixtures/trace-items-attributes.json" with {
42 | type: "json",
43 | };
44 | import traceItemsAttributesSpansStringFixture from "./fixtures/trace-items-attributes-spans-string.json" with {
45 | type: "json",
46 | };
47 | import traceItemsAttributesSpansNumberFixture from "./fixtures/trace-items-attributes-spans-number.json" with {
48 | type: "json",
49 | };
50 | import traceItemsAttributesLogsStringFixture from "./fixtures/trace-items-attributes-logs-string.json" with {
51 | type: "json",
52 | };
53 | import traceItemsAttributesLogsNumberFixture from "./fixtures/trace-items-attributes-logs-number.json" with {
54 | type: "json",
55 | };
56 | import traceMetaFixture from "./fixtures/trace-meta.json" with { type: "json" };
57 | import traceMetaWithNullsFixture from "./fixtures/trace-meta-with-nulls.json" with {
58 | type: "json",
59 | };
60 | import traceFixture from "./fixtures/trace.json" with { type: "json" };
61 | import traceMixedFixture from "./fixtures/trace-mixed.json" with {
62 | type: "json",
63 | };
64 | import traceEventFixture from "./fixtures/trace-event.json" with {
65 | type: "json",
66 | };
67 |
68 | /**
69 | * Standard organization payload for mock responses.
70 | * Used across multiple endpoints for consistency.
71 | */
72 | const OrganizationPayload = {
73 | id: "4509106740723712",
74 | slug: "sentry-mcp-evals",
75 | name: "sentry-mcp-evals",
76 | links: {
77 | regionUrl: "https://us.sentry.io",
78 | organizationUrl: "https://sentry.io/sentry-mcp-evals",
79 | },
80 | };
81 |
82 | /**
83 | * Standard release payload for mock responses.
84 | * Includes typical metadata and project associations.
85 | */
86 | const ReleasePayload = {
87 | id: 1402755016,
88 | version: "8ce89484-0fec-4913-a2cd-e8e2d41dee36",
89 | status: "open",
90 | shortVersion: "8ce89484-0fec-4913-a2cd-e8e2d41dee36",
91 | versionInfo: {
92 | package: null,
93 | version: { raw: "8ce89484-0fec-4913-a2cd-e8e2d41dee36" },
94 | description: "8ce89484-0fec-4913-a2cd-e8e2d41dee36",
95 | buildHash: null,
96 | },
97 | ref: null,
98 | url: null,
99 | dateReleased: null,
100 | dateCreated: "2025-04-13T19:54:21.764000Z",
101 | data: {},
102 | newGroups: 0,
103 | owner: null,
104 | commitCount: 0,
105 | lastCommit: null,
106 | deployCount: 0,
107 | lastDeploy: null,
108 | authors: [],
109 | projects: [
110 | {
111 | id: 4509062593708032,
112 | slug: "cloudflare-mcp",
113 | name: "cloudflare-mcp",
114 | newGroups: 0,
115 | platform: "bun",
116 | platforms: ["javascript"],
117 | hasHealthData: false,
118 | },
119 | ],
120 | firstEvent: "2025-04-13T19:54:21Z",
121 | lastEvent: "2025-04-13T20:28:23Z",
122 | currentProjectMeta: {},
123 | userAgent: null,
124 | };
125 |
126 | const ClientKeyPayload = {
127 | id: "d20df0a1ab5031c7f3c7edca9c02814d",
128 | name: "Default",
129 | label: "Default",
130 | public: "d20df0a1ab5031c7f3c7edca9c02814d",
131 | secret: "154001fd3dfe38130e1c7948a323fad8",
132 | projectId: 4509109104082945,
133 | isActive: true,
134 | rateLimit: null,
135 | dsn: {
136 | secret:
137 | "https://d20df0a1ab5031c7f3c7edca9c02814d:154001fd3dfe38130e1c7948a323fad8@o4509106732793856.ingest.us.sentry.io/4509109104082945",
138 | public:
139 | "https://d20df0a1ab5031c7f3c7edca9c02814d@o4509106732793856.ingest.us.sentry.io/4509109104082945",
140 | csp: "https://o4509106732793856.ingest.us.sentry.io/api/4509109104082945/csp-report/?sentry_key=d20df0a1ab5031c7f3c7edca9c02814d",
141 | security:
142 | "https://o4509106732793856.ingest.us.sentry.io/api/4509109104082945/security/?sentry_key=d20df0a1ab5031c7f3c7edca9c02814d",
143 | minidump:
144 | "https://o4509106732793856.ingest.us.sentry.io/api/4509109104082945/minidump/?sentry_key=d20df0a1ab5031c7f3c7edca9c02814d",
145 | nel: "https://o4509106732793856.ingest.us.sentry.io/api/4509109104082945/nel/?sentry_key=d20df0a1ab5031c7f3c7edca9c02814d",
146 | unreal:
147 | "https://o4509106732793856.ingest.us.sentry.io/api/4509109104082945/unreal/d20df0a1ab5031c7f3c7edca9c02814d/",
148 | crons:
149 | "https://o4509106732793856.ingest.us.sentry.io/api/4509109104082945/cron/___MONITOR_SLUG___/d20df0a1ab5031c7f3c7edca9c02814d/",
150 | cdn: "https://js.sentry-cdn.com/d20df0a1ab5031c7f3c7edca9c02814d.min.js",
151 | },
152 | browserSdkVersion: "8.x",
153 | browserSdk: {
154 | choices: [
155 | ["9.x", "9.x"],
156 | ["8.x", "8.x"],
157 | ["7.x", "7.x"],
158 | ],
159 | },
160 | dateCreated: "2025-04-07T00:12:25.139394Z",
161 | dynamicSdkLoaderOptions: {
162 | hasReplay: true,
163 | hasPerformance: true,
164 | hasDebug: false,
165 | },
166 | };
167 |
168 | // a newer issue, seen less recently
169 | const issueFixture2 = {
170 | ...issueFixture,
171 | id: 6507376926,
172 | shortId: "CLOUDFLARE-MCP-42",
173 | count: 1,
174 | title: "Error: Tool list_issues is already registered",
175 | firstSeen: "2025-04-11T22:51:19.403000Z",
176 | lastSeen: "2025-04-12T11:34:11Z",
177 | };
178 |
179 | const EventsErrorsMeta = {
180 | fields: {
181 | "issue.id": "integer",
182 | title: "string",
183 | project: "string",
184 | "count()": "integer",
185 | "last_seen()": "date",
186 | },
187 | units: {
188 | "issue.id": null,
189 | title: null,
190 | project: null,
191 | "count()": null,
192 | "last_seen()": null,
193 | },
194 | isMetricsData: false,
195 | isMetricsExtractedData: false,
196 | tips: { query: null, columns: null },
197 | datasetReason: "unchanged",
198 | dataset: "errors",
199 | };
200 |
201 | const EmptyEventsErrorsPayload = {
202 | data: [],
203 | meta: EventsErrorsMeta,
204 | };
205 |
206 | const EventsErrorsPayload = {
207 | data: [
208 | {
209 | "issue.id": 6114575469,
210 | title: "Error: Tool list_organizations is already registered",
211 | project: "test-suite",
212 | "count()": 2,
213 | "last_seen()": "2025-04-07T12:23:39+00:00",
214 | issue: "CLOUDFLARE-MCP-41",
215 | },
216 | ],
217 | meta: EventsErrorsMeta,
218 | };
219 |
220 | const EventsSpansMeta = {
221 | fields: {
222 | id: "string",
223 | "span.op": "string",
224 | "span.description": "string",
225 | "span.duration": "duration",
226 | transaction: "string",
227 | timestamp: "string",
228 | is_transaction: "boolean",
229 | project: "string",
230 | trace: "string",
231 | "transaction.span_id": "string",
232 | "project.name": "string",
233 | },
234 | units: {
235 | id: null,
236 | "span.op": null,
237 | "span.description": null,
238 | "span.duration": "millisecond",
239 | transaction: null,
240 | timestamp: null,
241 | is_transaction: null,
242 | project: null,
243 | trace: null,
244 | "transaction.span_id": null,
245 | "project.name": null,
246 | },
247 | isMetricsData: false,
248 | isMetricsExtractedData: false,
249 | tips: {},
250 | datasetReason: "unchanged",
251 | dataset: "spans",
252 | dataScanned: "full",
253 | accuracy: {
254 | confidence: [
255 | {},
256 | {},
257 | {},
258 | {},
259 | {},
260 | {},
261 | {},
262 | {},
263 | {},
264 | {},
265 | {},
266 | {},
267 | {},
268 | {},
269 | {},
270 | {},
271 | {},
272 | {},
273 | {},
274 | {},
275 | {},
276 | {},
277 | {},
278 | {},
279 | {},
280 | {},
281 | ],
282 | },
283 | };
284 |
285 | const EmptyEventsSpansPayload = {
286 | data: [],
287 | meta: EventsSpansMeta,
288 | };
289 |
290 | const EventsSpansPayload = {
291 | data: [
292 | {
293 | id: "07752c6aeb027c8f",
294 | "span.op": "http.server",
295 | "span.description": "GET /trpc/bottleList",
296 | "span.duration": 12.0,
297 | transaction: "GET /trpc/bottleList",
298 | timestamp: "2025-04-13T14:19:18+00:00",
299 | is_transaction: true,
300 | project: "peated",
301 | trace: "6a477f5b0f31ef7b6b9b5e1dea66c91d",
302 | "transaction.span_id": "07752c6aeb027c8f",
303 | "project.name": "peated",
304 | },
305 | {
306 | id: "7ab5edf5b3ba42c9",
307 | "span.op": "http.server",
308 | "span.description": "GET /trpc/bottleList",
309 | "span.duration": 18.0,
310 | transaction: "GET /trpc/bottleList",
311 | timestamp: "2025-04-13T14:19:17+00:00",
312 | is_transaction: true,
313 | project: "peated",
314 | trace: "54177131c7b192a446124daba3136045",
315 | "transaction.span_id": "7ab5edf5b3ba42c9",
316 | "project.name": "peated",
317 | },
318 | ],
319 | meta: EventsSpansMeta,
320 | confidence: [
321 | {},
322 | {},
323 | {},
324 | {},
325 | {},
326 | {},
327 | {},
328 | {},
329 | {},
330 | {},
331 | {},
332 | {},
333 | {},
334 | {},
335 | {},
336 | {},
337 | {},
338 | {},
339 | {},
340 | {},
341 | {},
342 | {},
343 | {},
344 | {},
345 | {},
346 | {},
347 | ],
348 | };
349 |
350 | /**
351 | * Builds MSW handlers for both SaaS and self-hosted Sentry instances.
352 | *
353 | * Creates handlers based on the controlOnly flag:
354 | * - controlOnly: false (default) - Creates handlers for both sentry.io and us.sentry.io
355 | * - controlOnly: true - Creates handlers only for sentry.io (main host)
356 | *
357 | * @param handlers - Array of handler definitions with method, path, fetch function, and optional controlOnly flag
358 | * @returns Array of MSW http handlers
359 | *
360 | * @example Handler Definitions
361 | * ```typescript
362 | * buildHandlers([
363 | * {
364 | * method: "get",
365 | * path: "/api/0/auth/",
366 | * fetch: () => HttpResponse.json({ user: "data" }),
367 | * controlOnly: true // Only available on sentry.io
368 | * },
369 | * {
370 | * method: "get",
371 | * path: "/api/0/organizations/",
372 | * fetch: () => HttpResponse.json([OrganizationPayload]),
373 | * controlOnly: false // Available on both sentry.io and us.sentry.io
374 | * }
375 | * ]);
376 | * ```
377 | */
378 | function buildHandlers(
379 | handlers: {
380 | method: keyof typeof http;
381 | path: string;
382 | fetch: Parameters<(typeof http)[keyof typeof http]>[1];
383 | controlOnly?: boolean;
384 | }[],
385 | ) {
386 | const result = [];
387 |
388 | for (const handler of handlers) {
389 | // Always add handler for main host (sentry.io)
390 | result.push(
391 | http[handler.method](`https://sentry.io${handler.path}`, handler.fetch),
392 | );
393 |
394 | // Only add handler for region-specific host if not controlOnly
395 | if (!handler.controlOnly) {
396 | result.push(
397 | http[handler.method](
398 | `https://us.sentry.io${handler.path}`,
399 | handler.fetch,
400 | ),
401 | );
402 | }
403 | }
404 |
405 | return result;
406 | }
407 |
408 | /**
409 | * Complete set of Sentry API mock handlers.
410 | *
411 | * Covers all endpoints used by the MCP server with realistic responses,
412 | * parameter validation, and error scenarios.
413 | */
414 | export const restHandlers = buildHandlers([
415 | // User data endpoints - controlOnly: true (only available on sentry.io)
416 | {
417 | method: "get",
418 | path: "/api/0/auth/",
419 | controlOnly: true,
420 | fetch: () => {
421 | return HttpResponse.json({
422 | id: "123456",
423 | name: "Test User",
424 | email: "[email protected]",
425 | username: "testuser",
426 | avatarUrl: "https://example.com/avatar.jpg",
427 | dateJoined: "2024-01-01T00:00:00Z",
428 | isActive: true,
429 | isManaged: false,
430 | isStaff: false,
431 | isSuperuser: false,
432 | lastLogin: "2024-12-01T00:00:00Z",
433 | has2fa: false,
434 | hasPasswordAuth: true,
435 | emails: [
436 | {
437 | id: "1",
438 | email: "[email protected]",
439 | is_verified: true,
440 | },
441 | ],
442 | });
443 | },
444 | },
445 | {
446 | method: "get",
447 | path: "/api/0/users/me/regions/",
448 | controlOnly: true,
449 | fetch: () => {
450 | return HttpResponse.json({
451 | regions: [{ name: "us", url: "https://us.sentry.io" }],
452 | });
453 | },
454 | },
455 | // All other endpoints - controlOnly: false (default, available on both hosts)
456 | {
457 | method: "get",
458 | path: "/api/0/organizations/",
459 | fetch: () => {
460 | return HttpResponse.json([OrganizationPayload]);
461 | },
462 | },
463 | {
464 | method: "get",
465 | path: "/api/0/organizations/sentry-mcp-evals/",
466 | fetch: () => {
467 | return HttpResponse.json(OrganizationPayload);
468 | },
469 | },
470 | // 404 handlers for test scenarios
471 | {
472 | method: "get",
473 | path: "/api/0/organizations/nonexistent-org/",
474 | fetch: () => {
475 | return HttpResponse.json(
476 | { detail: "The requested resource does not exist" },
477 | { status: 404 },
478 | );
479 | },
480 | },
481 | {
482 | method: "get",
483 | path: "/api/0/projects/sentry-mcp-evals/nonexistent-project/",
484 | fetch: () => {
485 | return HttpResponse.json(
486 | { detail: "The requested resource does not exist" },
487 | { status: 404 },
488 | );
489 | },
490 | },
491 | {
492 | method: "get",
493 | path: "/api/0/organizations/sentry-mcp-evals/teams/",
494 | fetch: () => {
495 | return HttpResponse.json([teamFixture]);
496 | },
497 | },
498 | {
499 | method: "get",
500 | path: "/api/0/organizations/sentry-mcp-evals/projects/",
501 | fetch: () => {
502 | return HttpResponse.json([
503 | {
504 | ...projectFixture,
505 | id: "4509106749636608", // Different ID for GET endpoint
506 | },
507 | ]);
508 | },
509 | },
510 | {
511 | method: "post",
512 | path: "/api/0/organizations/sentry-mcp-evals/teams/",
513 | fetch: () => {
514 | // TODO: validate payload (only accept 'the-goats' for team name)
515 | return HttpResponse.json(
516 | {
517 | ...teamFixture,
518 | id: "4509109078196224",
519 | dateCreated: "2025-04-07T00:05:48.196710Z",
520 | access: [
521 | "event:read",
522 | "org:integrations",
523 | "org:read",
524 | "member:read",
525 | "alerts:write",
526 | "event:admin",
527 | "team:admin",
528 | "project:releases",
529 | "team:read",
530 | "project:write",
531 | "event:write",
532 | "team:write",
533 | "project:read",
534 | "project:admin",
535 | "alerts:read",
536 | ],
537 | },
538 | { status: 201 },
539 | );
540 | },
541 | },
542 | {
543 | method: "post",
544 | path: "/api/0/teams/sentry-mcp-evals/the-goats/projects/",
545 | fetch: async ({ request }) => {
546 | // TODO: validate payload (only accept 'cloudflare-mcp' for project name)
547 | const body = (await request.json()) as any;
548 | return HttpResponse.json({
549 | ...projectFixture,
550 | name: body?.name || "cloudflare-mcp",
551 | slug: body?.slug || "cloudflare-mcp",
552 | platform: body?.platform || "node",
553 | });
554 | },
555 | },
556 | {
557 | method: "get",
558 | path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/",
559 | fetch: () => {
560 | return HttpResponse.json(projectFixture);
561 | },
562 | },
563 | {
564 | method: "put",
565 | path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/",
566 | fetch: async ({ request }) => {
567 | const body = (await request.json()) as any;
568 | return HttpResponse.json({
569 | ...projectFixture,
570 | slug: body?.slug || "cloudflare-mcp",
571 | name: body?.name || "cloudflare-mcp",
572 | platform: body?.platform || "node",
573 | });
574 | },
575 | },
576 | {
577 | method: "post",
578 | path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/keys/",
579 | fetch: () => {
580 | // TODO: validate payload (only accept 'Default' for key name)
581 | return HttpResponse.json(ClientKeyPayload);
582 | },
583 | },
584 | {
585 | method: "get",
586 | path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/keys/",
587 | fetch: () => {
588 | return HttpResponse.json([ClientKeyPayload]);
589 | },
590 | },
591 | {
592 | method: "get",
593 | path: "/api/0/organizations/sentry-mcp-evals/events/",
594 | fetch: async ({ request }) => {
595 | const url = new URL(request.url);
596 | const dataset = url.searchParams.get("dataset");
597 | const query = url.searchParams.get("query");
598 | const fields = url.searchParams.getAll("field");
599 |
600 | if (dataset === "spans") {
601 | //[sentryApi] GET https://sentry.io/api/0/organizations/sentry-mcp-evals/events/?dataset=spans&per_page=10&referrer=sentry-mcp&sort=-span.duration&allowAggregateConditions=0&useRpc=1&field=id&field=trace&field=span.op&field=span.description&field=span.duration&field=transaction&field=project&field=timestamp&query=is_transaction%3Atrue
602 | if (query !== "is_transaction:true") {
603 | return HttpResponse.json(EmptyEventsSpansPayload);
604 | }
605 |
606 | if (url.searchParams.get("useRpc") !== "1") {
607 | return HttpResponse.json("Invalid useRpc", { status: 400 });
608 | }
609 |
610 | if (
611 | !fields.includes("id") ||
612 | !fields.includes("trace") ||
613 | !fields.includes("span.op") ||
614 | !fields.includes("span.description") ||
615 | !fields.includes("span.duration")
616 | ) {
617 | return HttpResponse.json("Invalid fields", { status: 400 });
618 | }
619 | return HttpResponse.json(EventsSpansPayload);
620 | }
621 | if (dataset === "errors") {
622 | //https://sentry.io/api/0/organizations/sentry-mcp-evals/events/?dataset=errors&per_page=10&referrer=sentry-mcp&sort=-count&statsPeriod=1w&field=issue&field=title&field=project&field=last_seen%28%29&field=count%28%29&query=
623 |
624 | if (
625 | !fields.includes("issue") ||
626 | !fields.includes("title") ||
627 | !fields.includes("project") ||
628 | !fields.includes("last_seen()") ||
629 | !fields.includes("count()")
630 | ) {
631 | return HttpResponse.json("Invalid fields", { status: 400 });
632 | }
633 |
634 | if (
635 | !["-count", "-last_seen"].includes(
636 | url.searchParams.get("sort") as string,
637 | )
638 | ) {
639 | return HttpResponse.json("Invalid sort", { status: 400 });
640 | }
641 |
642 | // TODO: this is not correct, but itll fix test flakiness for now
643 | const sortedQuery = query ? query?.split(" ").sort().join(" ") : null;
644 | if (
645 | ![
646 | null,
647 | "",
648 | "error.handled:false",
649 | "error.unhandled:true",
650 | "error.handled:false is:unresolved",
651 | "error.unhandled:true is:unresolved",
652 | "is:unresolved project:cloudflare-mcp",
653 | "project:cloudflare-mcp",
654 | "user.email:[email protected]",
655 | ].includes(sortedQuery)
656 | ) {
657 | return HttpResponse.json(EmptyEventsErrorsPayload);
658 | }
659 |
660 | return HttpResponse.json(EventsErrorsPayload);
661 | }
662 |
663 | return HttpResponse.json("Invalid dataset", { status: 400 });
664 | },
665 | },
666 | {
667 | method: "get",
668 | path: "/api/0/projects/sentry-mcp-evals/foobar/issues/",
669 | fetch: () => HttpResponse.json([]),
670 | },
671 | {
672 | method: "get",
673 | path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/issues/",
674 | fetch: ({ request }) => {
675 | const url = new URL(request.url);
676 | const sort = url.searchParams.get("sort");
677 |
678 | if (![null, "user", "freq", "date", "new", null].includes(sort)) {
679 | return HttpResponse.json(
680 | `Invalid sort: ${url.searchParams.get("sort")}`,
681 | {
682 | status: 400,
683 | },
684 | );
685 | }
686 |
687 | const collapse = url.searchParams.getAll("collapse");
688 | if (collapse.includes("stats")) {
689 | return HttpResponse.json(`Invalid collapse: ${collapse.join(",")}`, {
690 | status: 400,
691 | });
692 | }
693 |
694 | const query = url.searchParams.get("query");
695 | const queryTokens = query?.split(" ").sort() ?? [];
696 | const sortedQuery = queryTokens ? queryTokens.join(" ") : null;
697 | if (
698 | ![
699 | null,
700 | "",
701 | "is:unresolved",
702 | "error.handled:false is:unresolved",
703 | "error.unhandled:true is:unresolved",
704 | "user.email:[email protected]",
705 | ].includes(sortedQuery)
706 | ) {
707 | return HttpResponse.json([]);
708 | }
709 |
710 | if (queryTokens.includes("user.email:[email protected]")) {
711 | return HttpResponse.json([issueFixture]);
712 | }
713 |
714 | if (sort === "date") {
715 | return HttpResponse.json([issueFixture, issueFixture2]);
716 | }
717 | return HttpResponse.json([issueFixture2, issueFixture]);
718 | },
719 | },
720 |
721 | {
722 | method: "get",
723 | path: "/api/0/organizations/sentry-mcp-evals/issues/",
724 | fetch: ({ request }) => {
725 | const url = new URL(request.url);
726 | const sort = url.searchParams.get("sort");
727 |
728 | if (![null, "user", "freq", "date", "new", null].includes(sort)) {
729 | return HttpResponse.json(
730 | `Invalid sort: ${url.searchParams.get("sort")}`,
731 | {
732 | status: 400,
733 | },
734 | );
735 | }
736 |
737 | const collapse = url.searchParams.getAll("collapse");
738 | if (collapse.includes("stats")) {
739 | return HttpResponse.json(`Invalid collapse: ${collapse.join(",")}`, {
740 | status: 400,
741 | });
742 | }
743 |
744 | const query = url.searchParams.get("query");
745 | const queryTokens = query?.split(" ").sort() ?? [];
746 | const sortedQuery = queryTokens ? queryTokens.join(" ") : null;
747 | if (query === "7ca573c0f4814912aaa9bdc77d1a7d51") {
748 | return HttpResponse.json([issueFixture]);
749 | }
750 | if (
751 | ![
752 | null,
753 | "",
754 | "is:unresolved",
755 | "error.handled:false is:unresolved",
756 | "error.unhandled:true is:unresolved",
757 | "project:cloudflare-mcp",
758 | "is:unresolved project:cloudflare-mcp",
759 | "user.email:[email protected]",
760 | ].includes(sortedQuery)
761 | ) {
762 | if (queryTokens.includes("project:remote-mcp")) {
763 | return HttpResponse.json(
764 | {
765 | detail:
766 | "Invalid query. Project(s) remote-mcp do not exist or are not actively selected.",
767 | },
768 | { status: 400 },
769 | );
770 | }
771 | return HttpResponse.json([]);
772 | }
773 | if (queryTokens.includes("user.email:[email protected]")) {
774 | return HttpResponse.json([issueFixture]);
775 | }
776 |
777 | if (sort === "date") {
778 | return HttpResponse.json([issueFixture, issueFixture2]);
779 | }
780 | return HttpResponse.json([issueFixture2, issueFixture]);
781 | },
782 | },
783 | {
784 | method: "get",
785 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/",
786 | fetch: () => HttpResponse.json(issueFixture),
787 | },
788 | {
789 | method: "get",
790 | path: "/api/0/organizations/sentry-mcp-evals/issues/6507376925/",
791 | fetch: () => HttpResponse.json(issueFixture),
792 | },
793 | {
794 | method: "get",
795 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-42/",
796 | fetch: () => HttpResponse.json(issueFixture2),
797 | },
798 | {
799 | method: "get",
800 | path: "/api/0/organizations/sentry-mcp-evals/issues/6507376926/",
801 | fetch: () => HttpResponse.json(issueFixture2),
802 | },
803 |
804 | // Trace endpoints
805 | {
806 | method: "get",
807 | path: "/api/0/organizations/sentry-mcp-evals/trace-meta/a4d1aae7216b47ff8117cf4e09ce9d0a/",
808 | fetch: () => HttpResponse.json(traceMetaFixture),
809 | },
810 | {
811 | method: "get",
812 | path: "/api/0/organizations/sentry-mcp-evals/trace/a4d1aae7216b47ff8117cf4e09ce9d0a/",
813 | fetch: () => HttpResponse.json(traceFixture),
814 | },
815 |
816 | {
817 | method: "get",
818 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/7ca573c0f4814912aaa9bdc77d1a7d51/",
819 | fetch: () => HttpResponse.json(eventsFixture),
820 | },
821 | {
822 | method: "get",
823 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/",
824 | fetch: () => HttpResponse.json(eventsFixture),
825 | },
826 | {
827 | method: "get",
828 | path: "/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/7ca573c0f4814912aaa9bdc77d1a7d51/",
829 | fetch: () => HttpResponse.json(eventsFixture),
830 | },
831 | {
832 | method: "get",
833 | path: "/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/",
834 | fetch: () => HttpResponse.json(eventsFixture),
835 | },
836 | // TODO: event payload should be tweaked to match issue
837 | {
838 | method: "get",
839 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-42/events/latest/",
840 | fetch: () => HttpResponse.json(eventsFixture),
841 | },
842 | // TODO: event payload should be tweaked to match issue
843 | {
844 | method: "get",
845 | path: "/api/0/organizations/sentry-mcp-evals/issues/6507376926/events/latest/",
846 | fetch: () => HttpResponse.json(eventsFixture),
847 | },
848 |
849 | // Performance issue with N+1 query detection
850 | {
851 | method: "get",
852 | path: "/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/latest/",
853 | fetch: () => HttpResponse.json(performanceEventFixture),
854 | },
855 | {
856 | method: "get",
857 | path: "/api/0/organizations/sentry-mcp-evals/issues/7890123456/events/latest/",
858 | fetch: () => HttpResponse.json(performanceEventFixture),
859 | },
860 | {
861 | method: "get",
862 | path: "/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/a1b2c3d4e5f6789012345678901234567/",
863 | fetch: () => HttpResponse.json(performanceEventFixture),
864 | },
865 |
866 | {
867 | method: "get",
868 | path: "/api/0/organizations/sentry-mcp-evals/releases/",
869 | fetch: () => HttpResponse.json([ReleasePayload]),
870 | },
871 | {
872 | method: "get",
873 | path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/releases/",
874 | fetch: () => HttpResponse.json([ReleasePayload]),
875 | },
876 | {
877 | method: "get",
878 | path: "/api/0/organizations/sentry-mcp-evals/tags/",
879 | fetch: () => HttpResponse.json(tagsFixture),
880 | },
881 | {
882 | method: "get",
883 | path: "/api/0/organizations/sentry-mcp-evals/trace-items/attributes/",
884 | fetch: ({ request }) => {
885 | const url = new URL(request.url);
886 | const itemType = url.searchParams.get("itemType");
887 | const attributeType = url.searchParams.get("attributeType");
888 |
889 | // Validate required parameters
890 | if (!itemType) {
891 | return HttpResponse.json(
892 | { detail: "itemType parameter is required" },
893 | { status: 400 },
894 | );
895 | }
896 |
897 | if (!attributeType) {
898 | return HttpResponse.json(
899 | { detail: "attributeType parameter is required" },
900 | { status: 400 },
901 | );
902 | }
903 |
904 | // Validate itemType values (API accepts both singular and plural forms)
905 | const normalizedItemType = itemType === "spans" ? "span" : itemType;
906 | if (!["span", "logs"].includes(normalizedItemType)) {
907 | return HttpResponse.json(
908 | {
909 | detail: `Invalid itemType '${itemType}'. Must be 'span' or 'logs'`,
910 | },
911 | { status: 400 },
912 | );
913 | }
914 |
915 | // Validate attributeType values
916 | if (!["string", "number"].includes(attributeType)) {
917 | return HttpResponse.json(
918 | {
919 | detail: `Invalid attributeType '${attributeType}'. Must be 'string' or 'number'`,
920 | },
921 | { status: 400 },
922 | );
923 | }
924 |
925 | // Return appropriate fixture based on parameters
926 | if (normalizedItemType === "span") {
927 | if (attributeType === "string") {
928 | return HttpResponse.json(traceItemsAttributesSpansStringFixture);
929 | }
930 | return HttpResponse.json(traceItemsAttributesSpansNumberFixture);
931 | }
932 | if (normalizedItemType === "logs") {
933 | if (attributeType === "string") {
934 | return HttpResponse.json(traceItemsAttributesLogsStringFixture);
935 | }
936 | return HttpResponse.json(traceItemsAttributesLogsNumberFixture);
937 | }
938 |
939 | // Fallback (should not reach here with valid inputs)
940 | return HttpResponse.json(traceItemsAttributesFixture);
941 | },
942 | },
943 | {
944 | method: "get",
945 | path: "/api/0/organizations/sentry-mcp-evals/issues/PEATED-A8/autofix/",
946 | fetch: () => HttpResponse.json(autofixStateFixture),
947 | },
948 | {
949 | method: "get",
950 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/",
951 | fetch: () => HttpResponse.json({ autofix: null }),
952 | },
953 | {
954 | method: "post",
955 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-42/autofix/",
956 | fetch: () => HttpResponse.json({ run_id: 123 }),
957 | },
958 | {
959 | method: "post",
960 | path: "/api/0/organizations/sentry-mcp-evals/issues/PEATED-A8/autofix/",
961 | fetch: () => HttpResponse.json({ run_id: 123 }),
962 | },
963 |
964 | {
965 | method: "get",
966 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-45/autofix/",
967 | fetch: () =>
968 | HttpResponse.json({
969 | autofix: {
970 | run_id: 13,
971 | request: { project_id: 4505138086019073 },
972 | status: "COMPLETED",
973 | updated_at: "2025-04-09T22:39:50.778146",
974 | steps: [
975 | {
976 | type: "root_cause_analysis",
977 | key: "root_cause_analysis",
978 | index: 0,
979 | status: "COMPLETED",
980 | title: "1. **Root Cause Analysis**",
981 | output_stream: null,
982 | progress: [],
983 | description: "The analysis has completed successfully.",
984 | causes: [
985 | {
986 | description: "The analysis has completed successfully.",
987 | id: 1,
988 | root_cause_reproduction: [],
989 | },
990 | ],
991 | },
992 | ],
993 | },
994 | }),
995 | },
996 | {
997 | method: "post",
998 | path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/teams/:teamSlug/",
999 | fetch: async ({ request, params }) => {
1000 | const body = (await request.json()) as any;
1001 | const teamSlug = params.teamSlug as string;
1002 | return HttpResponse.json({
1003 | ...teamFixture,
1004 | id: "4509109078196224",
1005 | slug: teamSlug,
1006 | name: teamSlug,
1007 | dateCreated: "2025-04-07T00:05:48.196710Z",
1008 | });
1009 | },
1010 | },
1011 | {
1012 | method: "put",
1013 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/",
1014 | fetch: async ({ request }) => {
1015 | const body = (await request.json()) as any;
1016 | const updatedIssue = {
1017 | ...issueFixture,
1018 | status: body?.status || issueFixture.status,
1019 | assignedTo: body?.assignedTo || issueFixture.assignedTo,
1020 | };
1021 | return HttpResponse.json(updatedIssue);
1022 | },
1023 | },
1024 | {
1025 | method: "put",
1026 | path: "/api/0/organizations/sentry-mcp-evals/issues/6507376925/",
1027 | fetch: async ({ request }) => {
1028 | const body = (await request.json()) as any;
1029 | const updatedIssue = {
1030 | ...issueFixture,
1031 | status: body?.status || issueFixture.status,
1032 | assignedTo: body?.assignedTo || issueFixture.assignedTo,
1033 | };
1034 | return HttpResponse.json(updatedIssue);
1035 | },
1036 | },
1037 | {
1038 | method: "put",
1039 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-42/",
1040 | fetch: async ({ request }) => {
1041 | const body = (await request.json()) as any;
1042 | const updatedIssue = {
1043 | ...issueFixture2,
1044 | status: body?.status || issueFixture2.status,
1045 | assignedTo: body?.assignedTo || issueFixture2.assignedTo,
1046 | };
1047 | return HttpResponse.json(updatedIssue);
1048 | },
1049 | },
1050 | {
1051 | method: "put",
1052 | path: "/api/0/organizations/sentry-mcp-evals/issues/6507376926/",
1053 | fetch: async ({ request }) => {
1054 | const body = (await request.json()) as any;
1055 | const updatedIssue = {
1056 | ...issueFixture2,
1057 | status: body?.status || issueFixture2.status,
1058 | assignedTo: body?.assignedTo || issueFixture2.assignedTo,
1059 | };
1060 | return HttpResponse.json(updatedIssue);
1061 | },
1062 | },
1063 | // Event attachment endpoints
1064 | {
1065 | method: "get",
1066 | path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/events/7ca573c0f4814912aaa9bdc77d1a7d51/attachments/",
1067 | fetch: () => HttpResponse.json(eventAttachmentsFixture),
1068 | },
1069 | {
1070 | method: "get",
1071 | path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/events/7ca573c0f4814912aaa9bdc77d1a7d51/attachments/123/",
1072 | fetch: () => {
1073 | // Mock attachment blob response
1074 | const mockBlob = new Blob(["fake image data"], { type: "image/png" });
1075 | return new HttpResponse(mockBlob, {
1076 | headers: {
1077 | "Content-Type": "image/png",
1078 | },
1079 | });
1080 | },
1081 | },
1082 | ]);
1083 |
1084 | // Add handlers for mcp.sentry.dev and localhost
1085 | export const searchHandlers = [
1086 | http.post("https://mcp.sentry.dev/api/search", async ({ request }) => {
1087 | const body = (await request.json()) as any;
1088 |
1089 | // Mock different results based on guide
1090 | let results = [
1091 | {
1092 | id: "product/rate-limiting.md",
1093 | url: "https://docs.sentry.io/product/rate-limiting",
1094 | snippet:
1095 | "Learn how to configure rate limiting in Sentry to prevent quota exhaustion and control event ingestion.",
1096 | relevance: 0.95,
1097 | },
1098 | {
1099 | id: "product/accounts/quotas/spike-protection.md",
1100 | url: "https://docs.sentry.io/product/accounts/quotas/spike-protection",
1101 | snippet:
1102 | "Spike protection helps prevent unexpected spikes in event volume from consuming your quota.",
1103 | relevance: 0.87,
1104 | },
1105 | ];
1106 |
1107 | // If guide is specified, return platform-specific results
1108 | if (body?.guide) {
1109 | const guide = body.guide;
1110 | if (guide.includes("/")) {
1111 | const [platformName, guideName] = guide.split("/");
1112 | results = [
1113 | {
1114 | id: `platforms/${platformName}/guides/${guideName}.md`,
1115 | url: `https://docs.sentry.io/platforms/${platformName}/guides/${guideName}`,
1116 | snippet: `Setup guide for ${guideName} on ${platformName}`,
1117 | relevance: 0.95,
1118 | },
1119 | ];
1120 | } else {
1121 | results = [
1122 | {
1123 | id: `platforms/${guide}/index.md`,
1124 | url: `https://docs.sentry.io/platforms/${guide}`,
1125 | snippet: `Documentation for ${guide} platform`,
1126 | relevance: 0.95,
1127 | },
1128 | ];
1129 | }
1130 | }
1131 |
1132 | // Return mock search results
1133 | return HttpResponse.json({
1134 | query: body?.query || "",
1135 | results,
1136 | });
1137 | }),
1138 | ];
1139 |
1140 | // Mock handlers for documentation fetching
1141 | export const docsHandlers = [
1142 | http.get("https://docs.sentry.io/product/rate-limiting.md", () => {
1143 | return new HttpResponse(
1144 | `# Project Rate Limits and Quotas
1145 |
1146 | Rate limiting allows you to control the volume of events that Sentry accepts from your applications. This helps you manage costs and ensures that a sudden spike in errors doesn't consume your entire quota.
1147 |
1148 | ## Why Use Rate Limiting?
1149 |
1150 | - **Cost Control**: Prevent unexpected charges from error spikes
1151 | - **Noise Reduction**: Filter out repetitive or low-value events
1152 | - **Resource Management**: Ensure critical projects have quota available
1153 | - **Performance**: Reduce load on your Sentry organization
1154 |
1155 | ## Types of Rate Limits
1156 |
1157 | ### 1. Organization Rate Limits
1158 |
1159 | Set a maximum number of events per hour across your entire organization:
1160 |
1161 | \`\`\`python
1162 | # In your organization settings
1163 | rate_limit = 1000 # events per hour
1164 | \`\`\`
1165 |
1166 | ### 2. Project Rate Limits
1167 |
1168 | Configure limits for specific projects:
1169 |
1170 | \`\`\`javascript
1171 | // Project settings
1172 | {
1173 | "rateLimit": {
1174 | "window": 3600, // 1 hour in seconds
1175 | "limit": 500 // max events
1176 | }
1177 | }
1178 | \`\`\`
1179 |
1180 | ### 3. Key-Based Rate Limiting
1181 |
1182 | Rate limit by specific attributes:
1183 |
1184 | - **By Release**: Limit events from specific releases
1185 | - **By User**: Prevent single users from consuming quota
1186 | - **By Transaction**: Control high-volume transactions
1187 |
1188 | ## Configuration Examples
1189 |
1190 | ### SDK Configuration
1191 |
1192 | Configure client-side sampling to reduce events before they're sent:
1193 |
1194 | \`\`\`javascript
1195 | Sentry.init({
1196 | dsn: "your-dsn",
1197 | tracesSampleRate: 0.1, // Sample 10% of transactions
1198 | beforeSend(event) {
1199 | // Custom filtering logic
1200 | if (event.exception?.values?.[0]?.value?.includes("NetworkError")) {
1201 | return null; // Drop network errors
1202 | }
1203 | return event;
1204 | }
1205 | });
1206 | \`\`\`
1207 |
1208 | ### Inbound Filters
1209 |
1210 | Use Sentry's inbound filters to drop events server-side:
1211 |
1212 | 1. Go to **Project Settings** → **Inbound Filters**
1213 | 2. Enable filters for:
1214 | - Legacy browsers
1215 | - Web crawlers
1216 | - Specific error messages
1217 | - IP addresses
1218 |
1219 | ### Spike Protection
1220 |
1221 | Enable spike protection to automatically limit events during traffic spikes:
1222 |
1223 | \`\`\`python
1224 | # Project settings
1225 | spike_protection = {
1226 | "enabled": True,
1227 | "max_events_per_hour": 10000,
1228 | "detection_window": 300 # 5 minutes
1229 | }
1230 | \`\`\`
1231 |
1232 | ## Best Practices
1233 |
1234 | 1. **Start Conservative**: Begin with lower limits and increase as needed
1235 | 2. **Monitor Usage**: Regularly review your quota consumption
1236 | 3. **Use Sampling**: Implement transaction sampling for high-volume apps
1237 | 4. **Filter Noise**: Drop known low-value events at the SDK level
1238 | 5. **Set Alerts**: Configure notifications for quota thresholds
1239 |
1240 | ## Rate Limit Headers
1241 |
1242 | Sentry returns rate limit information in response headers:
1243 |
1244 | \`\`\`
1245 | X-Sentry-Rate-Limit: 60
1246 | X-Sentry-Rate-Limit-Remaining: 42
1247 | X-Sentry-Rate-Limit-Reset: 1634567890
1248 | \`\`\`
1249 |
1250 | ## Quota Management
1251 |
1252 | ### Viewing Quota Usage
1253 |
1254 | 1. Navigate to **Settings** → **Subscription**
1255 | 2. View usage by:
1256 | - Project
1257 | - Event type
1258 | - Time period
1259 |
1260 | ### On-Demand Budgets
1261 |
1262 | Purchase additional events when approaching limits:
1263 |
1264 | \`\`\`bash
1265 | # Via API
1266 | curl -X POST https://sentry.io/api/0/organizations/{org}/quotas/ \\
1267 | -H 'Authorization: Bearer <token>' \\
1268 | -d '{"events": 100000}'
1269 | \`\`\`
1270 |
1271 | ## Troubleshooting
1272 |
1273 | ### Events Being Dropped?
1274 |
1275 | Check:
1276 | 1. Organization and project rate limits
1277 | 2. Spike protection status
1278 | 3. SDK sampling configuration
1279 | 4. Inbound filter settings
1280 |
1281 | ### Rate Limit Errors
1282 |
1283 | If you see 429 errors:
1284 | - Review your rate limit configuration
1285 | - Implement exponential backoff
1286 | - Consider event buffering
1287 |
1288 | ## Related Documentation
1289 |
1290 | - [SDK Configuration Guide](/platforms/javascript/configuration)
1291 | - [Quotas and Billing](/product/quotas)
1292 | - [Filtering Events](/product/data-management/filtering)`,
1293 | {
1294 | headers: {
1295 | "Content-Type": "text/markdown",
1296 | },
1297 | },
1298 | );
1299 | }),
1300 | http.get(
1301 | "https://docs.sentry.io/product/accounts/quotas/spike-protection.md",
1302 | () => {
1303 | return new HttpResponse(
1304 | `# Spike Protection
1305 |
1306 | Spike protection prevents sudden spikes in event volume from consuming your entire quota.
1307 |
1308 | ## How it works
1309 |
1310 | When Sentry detects an abnormal spike in events, it automatically activates spike protection...`,
1311 | {
1312 | headers: {
1313 | "Content-Type": "text/markdown",
1314 | },
1315 | },
1316 | );
1317 | },
1318 | ),
1319 | // Catch-all for other doc paths - return 404
1320 | http.get("https://docs.sentry.io/*.md", () => {
1321 | return new HttpResponse(null, { status: 404 });
1322 | }),
1323 | ];
1324 |
1325 | /**
1326 | * Configured MSW server instance with all Sentry API mock handlers.
1327 | *
1328 | * Ready-to-use mock server for testing and development. Includes all endpoints
1329 | * with realistic data, parameter validation, and error scenarios.
1330 | *
1331 | * @example Test Setup
1332 | * ```typescript
1333 | * import { mswServer } from "@sentry/mcp-server-mocks";
1334 | *
1335 | * beforeAll(() => mswServer.listen({ onUnhandledRequest: 'error' }));
1336 | * afterEach(() => mswServer.resetHandlers());
1337 | * afterAll(() => mswServer.close());
1338 | * ```
1339 | *
1340 | * @example Development Usage
1341 | * ```typescript
1342 | * import { mswServer } from "@sentry/mcp-server-mocks";
1343 | *
1344 | * // Start intercepting requests
1345 | * mswServer.listen();
1346 | *
1347 | * // Your MCP server will now use mock responses
1348 | * const apiService = new SentryApiService({ host: "sentry.io" });
1349 | * const orgs = await apiService.listOrganizations();
1350 | * console.log(orgs); // Returns mock organization data
1351 | * ```
1352 | *
1353 | * @note User Data Endpoint Restrictions
1354 | * The following endpoints are configured with `controlOnly: true` to work ONLY
1355 | * with the main host (sentry.io) and will NOT respond to requests from
1356 | * region-specific hosts (us.sentry.io, de.sentry.io):
1357 | * - `/api/0/auth/` (whoami endpoint)
1358 | * - `/api/0/users/me/regions/` (find_organizations endpoint)
1359 | *
1360 | * This matches the real Sentry API behavior where user data must always be queried
1361 | * from the main API server.
1362 | */
1363 | export const mswServer = setupServer(
1364 | ...restHandlers,
1365 | ...searchHandlers,
1366 | ...docsHandlers,
1367 | );
1368 |
1369 | // Export fixtures for use in tests
1370 | export {
1371 | autofixStateFixture,
1372 | traceMetaFixture,
1373 | traceMetaWithNullsFixture,
1374 | performanceEventFixture,
1375 | traceFixture,
1376 | traceMixedFixture,
1377 | traceEventFixture,
1378 | };
1379 |
1380 | // Export utilities for creating mock servers
1381 | export { setupMockServer, startMockServer } from "./utils";
1382 |
```