This is page 17 of 20. 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.md
│ ├── api-patterns.md
│ ├── architecture.md
│ ├── cloudflare
│ │ ├── architecture.md
│ │ ├── oauth-architecture.md
│ │ └── overview.md
│ ├── coding-guidelines.md
│ ├── common-patterns.md
│ ├── error-handling.md
│ ├── github-actions.md
│ ├── llms
│ │ ├── document-scopes.md
│ │ ├── documentation-style-guide.md
│ │ └── README.md
│ ├── logging.md
│ ├── monitoring.md
│ ├── pr-management.md
│ ├── quality-checks.md
│ ├── README.md
│ ├── releases
│ │ ├── cloudflare.md
│ │ └── stdio.md
│ ├── search-events-api-patterns.md
│ ├── security.md
│ ├── specs
│ │ ├── README.md
│ │ ├── search-events.md
│ │ └── subpath-constraints.md
│ ├── testing-remote.md
│ ├── testing-stdio.md
│ ├── testing.md
│ └── token-cost-tracking.md
├── LICENSE.md
├── Makefile
├── package.json
├── packages
│ ├── mcp-cloudflare
│ │ ├── .env.example
│ │ ├── components.json
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── public
│ │ │ ├── demo.cast
│ │ │ ├── favicon.ico
│ │ │ ├── flow-transparent.png
│ │ │ ├── flow.jpg
│ │ │ ├── keycap-⌘.png
│ │ │ ├── keycap-c.png
│ │ │ └── keycap-v.png
│ │ ├── src
│ │ │ ├── client
│ │ │ │ ├── app.tsx
│ │ │ │ ├── components
│ │ │ │ │ ├── animation
│ │ │ │ │ │ ├── browser-ui
│ │ │ │ │ │ │ ├── BrowserWindow.tsx
│ │ │ │ │ │ │ ├── BrowserWindowIconSidebar.tsx
│ │ │ │ │ │ │ ├── DiffBlock.tsx
│ │ │ │ │ │ │ ├── IDEWindow.tsx
│ │ │ │ │ │ │ ├── IssueDetails.tsx
│ │ │ │ │ │ │ ├── keys-copy.tsx
│ │ │ │ │ │ │ ├── LoadingSquares.tsx
│ │ │ │ │ │ │ ├── RootCause.tsx
│ │ │ │ │ │ │ ├── seer-clipmask.tsx
│ │ │ │ │ │ │ ├── seer-noisefilter.tsx
│ │ │ │ │ │ │ ├── seer.tsx
│ │ │ │ │ │ │ └── WindowHeader.tsx
│ │ │ │ │ │ ├── BrowserAnimation.tsx
│ │ │ │ │ │ ├── DataWire.tsx
│ │ │ │ │ │ ├── dracula.css
│ │ │ │ │ │ ├── terminal-ui
│ │ │ │ │ │ │ ├── keys-paste.tsx
│ │ │ │ │ │ │ ├── SpeedDisplay.tsx
│ │ │ │ │ │ │ └── StepsList.tsx
│ │ │ │ │ │ ├── TerminalAnimation.tsx
│ │ │ │ │ │ └── tests.tsx
│ │ │ │ │ ├── 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
│ │ │ │ │ │ ├── install-tabs.tsx
│ │ │ │ │ │ ├── remote-setup.tsx
│ │ │ │ │ │ ├── setup-guide.tsx
│ │ │ │ │ │ └── stdio-setup.tsx
│ │ │ │ │ ├── getting-started.tsx
│ │ │ │ │ ├── hero
│ │ │ │ │ │ ├── header-divider.tsx
│ │ │ │ │ │ └── hero-block.tsx
│ │ │ │ │ ├── home-layout
│ │ │ │ │ │ ├── footer.tsx
│ │ │ │ │ │ └── sidebars.tsx
│ │ │ │ │ ├── ui
│ │ │ │ │ │ ├── accordion.tsx
│ │ │ │ │ │ ├── backdrop.tsx
│ │ │ │ │ │ ├── badge.tsx
│ │ │ │ │ │ ├── base.tsx
│ │ │ │ │ │ ├── button.tsx
│ │ │ │ │ │ ├── code-snippet.tsx
│ │ │ │ │ │ ├── header.tsx
│ │ │ │ │ │ ├── icon.tsx
│ │ │ │ │ │ ├── icons
│ │ │ │ │ │ │ ├── claude.tsx
│ │ │ │ │ │ │ ├── codex.tsx
│ │ │ │ │ │ │ ├── cursor.tsx
│ │ │ │ │ │ │ ├── gemini.tsx
│ │ │ │ │ │ │ ├── sentry.tsx
│ │ │ │ │ │ │ ├── vscode.tsx
│ │ │ │ │ │ │ ├── warp.tsx
│ │ │ │ │ │ │ ├── windsurf.tsx
│ │ │ │ │ │ │ └── zed.tsx
│ │ │ │ │ │ ├── interactive-markdown.tsx
│ │ │ │ │ │ ├── json-schema-params.tsx
│ │ │ │ │ │ ├── key-icon.tsx
│ │ │ │ │ │ ├── key-word.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
│ │ │ │ │ └── usecases
│ │ │ │ │ ├── fix-bugs.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── instrument.tsx
│ │ │ │ │ ├── search-things.tsx
│ │ │ │ │ └── search-visual.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
│ │ │ │ ├── utils
│ │ │ │ │ ├── chat-error-handler.ts
│ │ │ │ │ ├── cursor-deeplink.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
│ │ │ │ ├── client-ip.test.ts
│ │ │ │ ├── client-ip.ts
│ │ │ │ ├── rate-limiter.test.ts
│ │ │ │ └── rate-limiter.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-core
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── scripts
│ │ │ ├── generate-definitions.ts
│ │ │ ├── generate-otel-namespaces.ts
│ │ │ ├── measure-token-cost.ts
│ │ │ └── validate-skills-mapping.ts
│ │ ├── src
│ │ │ ├── api-client
│ │ │ │ ├── client.test.ts
│ │ │ │ ├── client.ts
│ │ │ │ ├── errors.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── schema.test.ts
│ │ │ │ ├── schema.ts
│ │ │ │ └── types.ts
│ │ │ ├── constants.ts
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── internal
│ │ │ │ ├── agents
│ │ │ │ │ ├── callEmbeddedAgent.ts
│ │ │ │ │ ├── openai-provider.test.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.ts
│ │ │ ├── skillDefinitions.json
│ │ │ ├── skillDefinitions.ts
│ │ │ ├── skills.test.ts
│ │ │ ├── skills.ts
│ │ │ ├── telem
│ │ │ │ ├── index.ts
│ │ │ │ ├── logging.ts
│ │ │ │ ├── sentry.test.ts
│ │ │ │ └── sentry.ts
│ │ │ ├── test-setup.ts
│ │ │ ├── test-utils
│ │ │ │ └── context.ts
│ │ │ ├── toolDefinitions.json
│ │ │ ├── toolDefinitions.ts
│ │ │ ├── tools
│ │ │ │ ├── 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
│ │ │ ├── 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
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── cli
│ │ │ │ ├── parse.test.ts
│ │ │ │ ├── parse.ts
│ │ │ │ ├── resolve.test.ts
│ │ │ │ ├── resolve.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── usage.ts
│ │ │ ├── index.ts
│ │ │ └── transports
│ │ │ └── stdio.ts
│ │ ├── tsconfig.json
│ │ └── tsdown.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
│ │ │ │ ├── csp-event.json
│ │ │ │ ├── csp-issue.json
│ │ │ │ ├── default-event.json
│ │ │ │ ├── event-attachments.json
│ │ │ │ ├── event.json
│ │ │ │ ├── generic-event.json
│ │ │ │ ├── issue.json
│ │ │ │ ├── performance-event.json
│ │ │ │ ├── performance-issue.json
│ │ │ │ ├── project.json
│ │ │ │ ├── regressed-issue.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
│ │ │ │ ├── unknown-event.json
│ │ │ │ └── unsupported-issue.json
│ │ │ ├── fixtures.ts
│ │ │ ├── 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-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 | method: "get",
965 | path: "/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/autofix/",
966 | fetch: () => HttpResponse.json({ autofix: null }),
967 | },
968 | {
969 | method: "get",
970 | path: "/api/0/organizations/sentry-mcp-evals/issues/DEFAULT-001/autofix/",
971 | fetch: () => HttpResponse.json({ autofix: null }),
972 | },
973 | {
974 | method: "get",
975 | path: "/api/0/organizations/sentry-mcp-evals/issues/CONTEXT-001/autofix/",
976 | fetch: () => HttpResponse.json({ autofix: null }),
977 | },
978 | {
979 | method: "get",
980 | path: "/api/0/organizations/sentry-mcp-evals/issues/MCP-SERVER-EQE/autofix/",
981 | fetch: () => HttpResponse.json({ autofix: null }),
982 | },
983 | {
984 | method: "get",
985 | path: "/api/0/organizations/sentry-mcp-evals/issues/FUTURE-TYPE-001/autofix/",
986 | fetch: () => HttpResponse.json({ autofix: null }),
987 | },
988 | {
989 | method: "get",
990 | path: "/api/0/organizations/sentry-mcp-evals/issues/BLOG-CSP-4XC/autofix/",
991 | fetch: () => HttpResponse.json({ autofix: null }),
992 | },
993 |
994 | {
995 | method: "get",
996 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-45/autofix/",
997 | fetch: () =>
998 | HttpResponse.json({
999 | autofix: {
1000 | run_id: 13,
1001 | request: { project_id: 4505138086019073 },
1002 | status: "COMPLETED",
1003 | updated_at: "2025-04-09T22:39:50.778146",
1004 | steps: [
1005 | {
1006 | type: "root_cause_analysis",
1007 | key: "root_cause_analysis",
1008 | index: 0,
1009 | status: "COMPLETED",
1010 | title: "1. **Root Cause Analysis**",
1011 | output_stream: null,
1012 | progress: [],
1013 | description: "The analysis has completed successfully.",
1014 | causes: [
1015 | {
1016 | description: "The analysis has completed successfully.",
1017 | id: 1,
1018 | root_cause_reproduction: [],
1019 | },
1020 | ],
1021 | },
1022 | ],
1023 | },
1024 | }),
1025 | },
1026 | {
1027 | method: "post",
1028 | path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/teams/:teamSlug/",
1029 | fetch: async ({ request, params }) => {
1030 | const body = (await request.json()) as any;
1031 | const teamSlug = params.teamSlug as string;
1032 | return HttpResponse.json({
1033 | ...teamFixture,
1034 | id: "4509109078196224",
1035 | slug: teamSlug,
1036 | name: teamSlug,
1037 | dateCreated: "2025-04-07T00:05:48.196710Z",
1038 | });
1039 | },
1040 | },
1041 | {
1042 | method: "put",
1043 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/",
1044 | fetch: async ({ request }) => {
1045 | const body = (await request.json()) as any;
1046 | const updatedIssue = {
1047 | ...issueFixture,
1048 | status: body?.status || issueFixture.status,
1049 | assignedTo: body?.assignedTo || issueFixture.assignedTo,
1050 | };
1051 | return HttpResponse.json(updatedIssue);
1052 | },
1053 | },
1054 | {
1055 | method: "put",
1056 | path: "/api/0/organizations/sentry-mcp-evals/issues/6507376925/",
1057 | fetch: async ({ request }) => {
1058 | const body = (await request.json()) as any;
1059 | const updatedIssue = {
1060 | ...issueFixture,
1061 | status: body?.status || issueFixture.status,
1062 | assignedTo: body?.assignedTo || issueFixture.assignedTo,
1063 | };
1064 | return HttpResponse.json(updatedIssue);
1065 | },
1066 | },
1067 | {
1068 | method: "put",
1069 | path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-42/",
1070 | fetch: async ({ request }) => {
1071 | const body = (await request.json()) as any;
1072 | const updatedIssue = {
1073 | ...issueFixture2,
1074 | status: body?.status || issueFixture2.status,
1075 | assignedTo: body?.assignedTo || issueFixture2.assignedTo,
1076 | };
1077 | return HttpResponse.json(updatedIssue);
1078 | },
1079 | },
1080 | {
1081 | method: "put",
1082 | path: "/api/0/organizations/sentry-mcp-evals/issues/6507376926/",
1083 | fetch: async ({ request }) => {
1084 | const body = (await request.json()) as any;
1085 | const updatedIssue = {
1086 | ...issueFixture2,
1087 | status: body?.status || issueFixture2.status,
1088 | assignedTo: body?.assignedTo || issueFixture2.assignedTo,
1089 | };
1090 | return HttpResponse.json(updatedIssue);
1091 | },
1092 | },
1093 | // Event attachment endpoints
1094 | {
1095 | method: "get",
1096 | path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/events/7ca573c0f4814912aaa9bdc77d1a7d51/attachments/",
1097 | fetch: () => HttpResponse.json(eventAttachmentsFixture),
1098 | },
1099 | {
1100 | method: "get",
1101 | path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/events/7ca573c0f4814912aaa9bdc77d1a7d51/attachments/123/",
1102 | fetch: () => {
1103 | // Mock attachment blob response
1104 | const mockBlob = new Blob(["fake image data"], { type: "image/png" });
1105 | return new HttpResponse(mockBlob, {
1106 | headers: {
1107 | "Content-Type": "image/png",
1108 | },
1109 | });
1110 | },
1111 | },
1112 | ]);
1113 |
1114 | // Add handlers for mcp.sentry.dev and localhost
1115 | export const searchHandlers = [
1116 | http.post("https://mcp.sentry.dev/api/search", async ({ request }) => {
1117 | const body = (await request.json()) as any;
1118 |
1119 | // Mock different results based on guide
1120 | let results = [
1121 | {
1122 | id: "product/rate-limiting.md",
1123 | url: "https://docs.sentry.io/product/rate-limiting",
1124 | snippet:
1125 | "Learn how to configure rate limiting in Sentry to prevent quota exhaustion and control event ingestion.",
1126 | relevance: 0.95,
1127 | },
1128 | {
1129 | id: "product/accounts/quotas/spike-protection.md",
1130 | url: "https://docs.sentry.io/product/accounts/quotas/spike-protection",
1131 | snippet:
1132 | "Spike protection helps prevent unexpected spikes in event volume from consuming your quota.",
1133 | relevance: 0.87,
1134 | },
1135 | ];
1136 |
1137 | // If guide is specified, return platform-specific results
1138 | if (body?.guide) {
1139 | const guide = body.guide;
1140 | if (guide.includes("/")) {
1141 | const [platformName, guideName] = guide.split("/");
1142 | results = [
1143 | {
1144 | id: `platforms/${platformName}/guides/${guideName}.md`,
1145 | url: `https://docs.sentry.io/platforms/${platformName}/guides/${guideName}`,
1146 | snippet: `Setup guide for ${guideName} on ${platformName}`,
1147 | relevance: 0.95,
1148 | },
1149 | ];
1150 | } else {
1151 | results = [
1152 | {
1153 | id: `platforms/${guide}/index.md`,
1154 | url: `https://docs.sentry.io/platforms/${guide}`,
1155 | snippet: `Documentation for ${guide} platform`,
1156 | relevance: 0.95,
1157 | },
1158 | ];
1159 | }
1160 | }
1161 |
1162 | // Return mock search results
1163 | return HttpResponse.json({
1164 | query: body?.query || "",
1165 | results,
1166 | });
1167 | }),
1168 | ];
1169 |
1170 | // Mock handlers for documentation fetching
1171 | export const docsHandlers = [
1172 | http.get("https://docs.sentry.io/product/rate-limiting.md", () => {
1173 | return new HttpResponse(
1174 | `# Project Rate Limits and Quotas
1175 |
1176 | 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.
1177 |
1178 | ## Why Use Rate Limiting?
1179 |
1180 | - **Cost Control**: Prevent unexpected charges from error spikes
1181 | - **Noise Reduction**: Filter out repetitive or low-value events
1182 | - **Resource Management**: Ensure critical projects have quota available
1183 | - **Performance**: Reduce load on your Sentry organization
1184 |
1185 | ## Types of Rate Limits
1186 |
1187 | ### 1. Organization Rate Limits
1188 |
1189 | Set a maximum number of events per hour across your entire organization:
1190 |
1191 | \`\`\`python
1192 | # In your organization settings
1193 | rate_limit = 1000 # events per hour
1194 | \`\`\`
1195 |
1196 | ### 2. Project Rate Limits
1197 |
1198 | Configure limits for specific projects:
1199 |
1200 | \`\`\`javascript
1201 | // Project settings
1202 | {
1203 | "rateLimit": {
1204 | "window": 3600, // 1 hour in seconds
1205 | "limit": 500 // max events
1206 | }
1207 | }
1208 | \`\`\`
1209 |
1210 | ### 3. Key-Based Rate Limiting
1211 |
1212 | Rate limit by specific attributes:
1213 |
1214 | - **By Release**: Limit events from specific releases
1215 | - **By User**: Prevent single users from consuming quota
1216 | - **By Transaction**: Control high-volume transactions
1217 |
1218 | ## Configuration Examples
1219 |
1220 | ### SDK Configuration
1221 |
1222 | Configure client-side sampling to reduce events before they're sent:
1223 |
1224 | \`\`\`javascript
1225 | Sentry.init({
1226 | dsn: "your-dsn",
1227 | tracesSampleRate: 0.1, // Sample 10% of transactions
1228 | beforeSend(event) {
1229 | // Custom filtering logic
1230 | if (event.exception?.values?.[0]?.value?.includes("NetworkError")) {
1231 | return null; // Drop network errors
1232 | }
1233 | return event;
1234 | }
1235 | });
1236 | \`\`\`
1237 |
1238 | ### Inbound Filters
1239 |
1240 | Use Sentry's inbound filters to drop events server-side:
1241 |
1242 | 1. Go to **Project Settings** → **Inbound Filters**
1243 | 2. Enable filters for:
1244 | - Legacy browsers
1245 | - Web crawlers
1246 | - Specific error messages
1247 | - IP addresses
1248 |
1249 | ### Spike Protection
1250 |
1251 | Enable spike protection to automatically limit events during traffic spikes:
1252 |
1253 | \`\`\`python
1254 | # Project settings
1255 | spike_protection = {
1256 | "enabled": True,
1257 | "max_events_per_hour": 10000,
1258 | "detection_window": 300 # 5 minutes
1259 | }
1260 | \`\`\`
1261 |
1262 | ## Best Practices
1263 |
1264 | 1. **Start Conservative**: Begin with lower limits and increase as needed
1265 | 2. **Monitor Usage**: Regularly review your quota consumption
1266 | 3. **Use Sampling**: Implement transaction sampling for high-volume apps
1267 | 4. **Filter Noise**: Drop known low-value events at the SDK level
1268 | 5. **Set Alerts**: Configure notifications for quota thresholds
1269 |
1270 | ## Rate Limit Headers
1271 |
1272 | Sentry returns rate limit information in response headers:
1273 |
1274 | \`\`\`
1275 | X-Sentry-Rate-Limit: 60
1276 | X-Sentry-Rate-Limit-Remaining: 42
1277 | X-Sentry-Rate-Limit-Reset: 1634567890
1278 | \`\`\`
1279 |
1280 | ## Quota Management
1281 |
1282 | ### Viewing Quota Usage
1283 |
1284 | 1. Navigate to **Settings** → **Subscription**
1285 | 2. View usage by:
1286 | - Project
1287 | - Event type
1288 | - Time period
1289 |
1290 | ### On-Demand Budgets
1291 |
1292 | Purchase additional events when approaching limits:
1293 |
1294 | \`\`\`bash
1295 | # Via API
1296 | curl -X POST https://sentry.io/api/0/organizations/{org}/quotas/ \\
1297 | -H 'Authorization: Bearer <token>' \\
1298 | -d '{"events": 100000}'
1299 | \`\`\`
1300 |
1301 | ## Troubleshooting
1302 |
1303 | ### Events Being Dropped?
1304 |
1305 | Check:
1306 | 1. Organization and project rate limits
1307 | 2. Spike protection status
1308 | 3. SDK sampling configuration
1309 | 4. Inbound filter settings
1310 |
1311 | ### Rate Limit Errors
1312 |
1313 | If you see 429 errors:
1314 | - Review your rate limit configuration
1315 | - Implement exponential backoff
1316 | - Consider event buffering
1317 |
1318 | ## Related Documentation
1319 |
1320 | - [SDK Configuration Guide](/platforms/javascript/configuration)
1321 | - [Quotas and Billing](/product/quotas)
1322 | - [Filtering Events](/product/data-management/filtering)`,
1323 | {
1324 | headers: {
1325 | "Content-Type": "text/markdown",
1326 | },
1327 | },
1328 | );
1329 | }),
1330 | http.get(
1331 | "https://docs.sentry.io/product/accounts/quotas/spike-protection.md",
1332 | () => {
1333 | return new HttpResponse(
1334 | `# Spike Protection
1335 |
1336 | Spike protection prevents sudden spikes in event volume from consuming your entire quota.
1337 |
1338 | ## How it works
1339 |
1340 | When Sentry detects an abnormal spike in events, it automatically activates spike protection...`,
1341 | {
1342 | headers: {
1343 | "Content-Type": "text/markdown",
1344 | },
1345 | },
1346 | );
1347 | },
1348 | ),
1349 | // Catch-all for other doc paths - return 404
1350 | http.get("https://docs.sentry.io/*.md", () => {
1351 | return new HttpResponse(null, { status: 404 });
1352 | }),
1353 | ];
1354 |
1355 | /**
1356 | * Configured MSW server instance with all Sentry API mock handlers.
1357 | *
1358 | * Ready-to-use mock server for testing and development. Includes all endpoints
1359 | * with realistic data, parameter validation, and error scenarios.
1360 | *
1361 | * @example Test Setup
1362 | * ```typescript
1363 | * import { mswServer } from "@sentry/mcp-server-mocks";
1364 | *
1365 | * beforeAll(() => mswServer.listen({ onUnhandledRequest: 'error' }));
1366 | * afterEach(() => mswServer.resetHandlers());
1367 | * afterAll(() => mswServer.close());
1368 | * ```
1369 | *
1370 | * @example Development Usage
1371 | * ```typescript
1372 | * import { mswServer } from "@sentry/mcp-server-mocks";
1373 | *
1374 | * // Start intercepting requests
1375 | * mswServer.listen();
1376 | *
1377 | * // Your MCP server will now use mock responses
1378 | * const apiService = new SentryApiService({ host: "sentry.io" });
1379 | * const orgs = await apiService.listOrganizations();
1380 | * console.log(orgs); // Returns mock organization data
1381 | * ```
1382 | *
1383 | * @note User Data Endpoint Restrictions
1384 | * The following endpoints are configured with `controlOnly: true` to work ONLY
1385 | * with the main host (sentry.io) and will NOT respond to requests from
1386 | * region-specific hosts (us.sentry.io, de.sentry.io):
1387 | * - `/api/0/auth/` (whoami endpoint)
1388 | * - `/api/0/users/me/regions/` (find_organizations endpoint)
1389 | *
1390 | * This matches the real Sentry API behavior where user data must always be queried
1391 | * from the main API server.
1392 | */
1393 | export const mswServer = setupServer(
1394 | ...restHandlers,
1395 | ...searchHandlers,
1396 | ...docsHandlers,
1397 | );
1398 |
1399 | // Export fixtures for use in tests
1400 | export {
1401 | autofixStateFixture,
1402 | traceMetaFixture,
1403 | traceMetaWithNullsFixture,
1404 | performanceEventFixture,
1405 | traceFixture,
1406 | traceMixedFixture,
1407 | traceEventFixture,
1408 | };
1409 |
1410 | // Export fixture factories
1411 | export {
1412 | createDefaultEvent,
1413 | createGenericEvent,
1414 | createUnknownEvent,
1415 | createPerformanceEvent,
1416 | createPerformanceIssue,
1417 | createRegressedIssue,
1418 | createUnsupportedIssue,
1419 | createCspIssue,
1420 | createCspEvent,
1421 | } from "./fixtures";
1422 |
1423 | // Export utilities for creating mock servers
1424 | export { setupMockServer, startMockServer } from "./utils";
1425 |
```
--------------------------------------------------------------------------------
/packages/mcp-core/src/tools/get-issue-details.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, it, expect } from "vitest";
2 | import { http, HttpResponse } from "msw";
3 | import {
4 | mswServer,
5 | createDefaultEvent,
6 | createGenericEvent,
7 | createUnknownEvent,
8 | createPerformanceEvent,
9 | createPerformanceIssue,
10 | createRegressedIssue,
11 | createUnsupportedIssue,
12 | createCspIssue,
13 | createCspEvent,
14 | } from "@sentry/mcp-server-mocks";
15 | import getIssueDetails from "./get-issue-details.js";
16 |
17 | const baseContext = {
18 | constraints: {
19 | organizationSlug: undefined,
20 | },
21 | accessToken: "access-token",
22 | userId: "1",
23 | };
24 |
25 | // Removed - now using createPerformanceIssue() factory from mocks
26 |
27 | // Removed - now using createPerformanceEvent() factory from mocks with overrides
28 |
29 | function createTraceResponseFixture() {
30 | return [
31 | {
32 | span_id: "root-span",
33 | event_id: "root-span",
34 | transaction_id: "root-span",
35 | project_id: "4509062593708032",
36 | project_slug: "cloudflare-mcp",
37 | profile_id: "",
38 | profiler_id: "",
39 | parent_span_id: null,
40 | start_timestamp: 0,
41 | end_timestamp: 1,
42 | measurements: {},
43 | duration: 1000,
44 | transaction: "/api/users",
45 | is_transaction: true,
46 | description: "GET /api/users",
47 | sdk_name: "sentry.python",
48 | op: "http.server",
49 | name: "GET /api/users",
50 | event_type: "transaction",
51 | additional_attributes: {},
52 | errors: [],
53 | occurrences: [],
54 | children: [
55 | {
56 | span_id: "parent123",
57 | event_id: "parent123",
58 | transaction_id: "parent123",
59 | project_id: "4509062593708032",
60 | project_slug: "cloudflare-mcp",
61 | profile_id: "",
62 | profiler_id: "",
63 | parent_span_id: "root-span",
64 | start_timestamp: 0.1,
65 | end_timestamp: 0.35,
66 | measurements: {},
67 | duration: 250,
68 | transaction: "/api/users",
69 | is_transaction: false,
70 | description: "GET /api/users handler",
71 | sdk_name: "sentry.python",
72 | op: "http.server",
73 | name: "GET /api/users handler",
74 | event_type: "span",
75 | additional_attributes: {},
76 | errors: [],
77 | occurrences: [],
78 | children: [
79 | {
80 | span_id: "span001",
81 | event_id: "span001",
82 | transaction_id: "span001",
83 | project_id: "4509062593708032",
84 | project_slug: "cloudflare-mcp",
85 | profile_id: "",
86 | profiler_id: "",
87 | parent_span_id: "parent123",
88 | start_timestamp: 0.15,
89 | end_timestamp: 0.16,
90 | measurements: {},
91 | duration: 10,
92 | transaction: "/api/users",
93 | is_transaction: false,
94 | description: "SELECT * FROM users WHERE id = 1",
95 | sdk_name: "sentry.python",
96 | op: "db.query",
97 | name: "SELECT * FROM users WHERE id = 1",
98 | event_type: "span",
99 | additional_attributes: {},
100 | errors: [],
101 | occurrences: [],
102 | children: [],
103 | },
104 | {
105 | span_id: "span002",
106 | event_id: "span002",
107 | transaction_id: "span002",
108 | project_id: "4509062593708032",
109 | project_slug: "cloudflare-mcp",
110 | profile_id: "",
111 | profiler_id: "",
112 | parent_span_id: "parent123",
113 | start_timestamp: 0.2,
114 | end_timestamp: 0.212,
115 | measurements: {},
116 | duration: 12,
117 | transaction: "/api/users",
118 | is_transaction: false,
119 | description: "SELECT * FROM users WHERE id = 2",
120 | sdk_name: "sentry.python",
121 | op: "db.query",
122 | name: "SELECT * FROM users WHERE id = 2",
123 | event_type: "span",
124 | additional_attributes: {},
125 | errors: [],
126 | occurrences: [],
127 | children: [],
128 | },
129 | {
130 | span_id: "span003",
131 | event_id: "span003",
132 | transaction_id: "span003",
133 | project_id: "4509062593708032",
134 | project_slug: "cloudflare-mcp",
135 | profile_id: "",
136 | profiler_id: "",
137 | parent_span_id: "parent123",
138 | start_timestamp: 0.24,
139 | end_timestamp: 0.255,
140 | measurements: {},
141 | duration: 15,
142 | transaction: "/api/users",
143 | is_transaction: false,
144 | description: "SELECT * FROM users WHERE id = 3",
145 | sdk_name: "sentry.python",
146 | op: "db.query",
147 | name: "SELECT * FROM users WHERE id = 3",
148 | event_type: "span",
149 | additional_attributes: {},
150 | errors: [],
151 | occurrences: [],
152 | children: [],
153 | },
154 | ],
155 | },
156 | ],
157 | },
158 | ];
159 | }
160 |
161 | describe("get_issue_details", () => {
162 | it("serializes with issueId", async () => {
163 | const result = await getIssueDetails.handler(
164 | {
165 | organizationSlug: "sentry-mcp-evals",
166 | issueId: "CLOUDFLARE-MCP-41",
167 | eventId: undefined,
168 | issueUrl: undefined,
169 | regionUrl: null,
170 | },
171 | {
172 | constraints: {
173 | organizationSlug: undefined,
174 | },
175 | accessToken: "access-token",
176 | userId: "1",
177 | },
178 | );
179 | expect(result).toMatchInlineSnapshot(`
180 | "# Issue CLOUDFLARE-MCP-41 in **sentry-mcp-evals**
181 |
182 | **Description**: Error: Tool list_organizations is already registered
183 | **Culprit**: Object.fetch(index)
184 | **First Seen**: 2025-04-03T22:51:19.403Z
185 | **Last Seen**: 2025-04-12T11:34:11.000Z
186 | **Occurrences**: 25
187 | **Users Impacted**: 1
188 | **Status**: unresolved
189 | **Substatus**: ongoing
190 | **Assigned To**: Jane Developer (User)
191 | **Issue Type**: error
192 | **Issue Category**: error
193 | **Platform**: javascript
194 | **Project**: CLOUDFLARE-MCP
195 | **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41
196 |
197 | ## Event Details
198 |
199 | **Event ID**: 7ca573c0f4814912aaa9bdc77d1a7d51
200 | **Type**: error
201 | **Occurred At**: 2025-04-08T21:15:04.000Z
202 |
203 | ### Error
204 |
205 | \`\`\`
206 | Error: Tool list_organizations is already registered
207 | \`\`\`
208 |
209 | **Stacktrace:**
210 | \`\`\`
211 | index.js:7809:27
212 | index.js:8029:24 (OAuthProviderImpl.fetch)
213 | index.js:19631:28 (Object.fetch)
214 | \`\`\`
215 |
216 | ### HTTP Request
217 |
218 | **Method:** GET
219 | **URL:** https://mcp.sentry.dev/sse
220 |
221 | ### Tags
222 |
223 | **environment**: development
224 | **handled**: no
225 | **level**: error
226 | **mechanism**: cloudflare
227 | **runtime.name**: cloudflare
228 | **url**: https://mcp.sentry.dev/sse
229 |
230 | ### Additional Context
231 |
232 | These are additional context provided by the user when they're instrumenting their application.
233 |
234 | **cloud_resource**
235 | cloud.provider: "cloudflare"
236 |
237 | **culture**
238 | timezone: "Europe/London"
239 |
240 | **runtime**
241 | name: "cloudflare"
242 |
243 | **trace**
244 | trace_id: "3032af8bcdfe4423b937fc5c041d5d82"
245 | span_id: "953da703d2a6f4c7"
246 | status: "unknown"
247 | client_sample_rate: 1
248 | sampled: true
249 |
250 | # Using this information
251 |
252 | - You can reference the IssueID in commit messages (e.g. \`Fixes CLOUDFLARE-MCP-41\`) to automatically close the issue when the commit is merged.
253 | - The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.
254 | "
255 | `);
256 | });
257 |
258 | it("displays team assignment correctly", async () => {
259 | // Override the issue fixture with a team assignment
260 | mswServer.use(
261 | http.get(
262 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/TEAM-ISSUE-001/",
263 | () =>
264 | HttpResponse.json({
265 | id: "123456789",
266 | shortId: "TEAM-ISSUE-001",
267 | title: "Test issue with team assignment",
268 | firstSeen: "2025-04-03T22:51:19.403Z",
269 | lastSeen: "2025-04-12T11:34:11Z",
270 | count: "10",
271 | userCount: 5,
272 | permalink:
273 | "https://sentry-mcp-evals.sentry.io/issues/TEAM-ISSUE-001",
274 | project: {
275 | id: "4509062593708032",
276 | slug: "test-project",
277 | name: "Test Project",
278 | },
279 | platform: "javascript",
280 | status: "unresolved",
281 | substatus: "ongoing",
282 | culprit: "app.main",
283 | type: "error",
284 | issueType: "error",
285 | issueCategory: "error",
286 | assignedTo: {
287 | type: "team",
288 | id: "99999",
289 | name: "Platform Team",
290 | },
291 | }),
292 | { once: true },
293 | ),
294 | http.get(
295 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/TEAM-ISSUE-001/events/latest/",
296 | () => HttpResponse.json(createDefaultEvent()),
297 | { once: true },
298 | ),
299 | );
300 |
301 | const result = await getIssueDetails.handler(
302 | {
303 | organizationSlug: "sentry-mcp-evals",
304 | issueId: "TEAM-ISSUE-001",
305 | eventId: undefined,
306 | issueUrl: undefined,
307 | regionUrl: null,
308 | },
309 | baseContext,
310 | );
311 |
312 | // Verify that team assignment is displayed with "(Team)" suffix
313 | expect(result).toContain("**Assigned To**: Platform Team (Team)");
314 | });
315 |
316 | it("serializes with issueUrl", async () => {
317 | const result = await getIssueDetails.handler(
318 | {
319 | organizationSlug: undefined,
320 | issueId: undefined,
321 | eventId: undefined,
322 | issueUrl: "https://sentry-mcp-evals.sentry.io/issues/6507376925",
323 | regionUrl: null,
324 | },
325 | {
326 | constraints: {
327 | organizationSlug: undefined,
328 | },
329 | accessToken: "access-token",
330 | userId: "1",
331 | },
332 | );
333 |
334 | expect(result).toMatchInlineSnapshot(`
335 | "# Issue CLOUDFLARE-MCP-41 in **sentry-mcp-evals**
336 |
337 | **Description**: Error: Tool list_organizations is already registered
338 | **Culprit**: Object.fetch(index)
339 | **First Seen**: 2025-04-03T22:51:19.403Z
340 | **Last Seen**: 2025-04-12T11:34:11.000Z
341 | **Occurrences**: 25
342 | **Users Impacted**: 1
343 | **Status**: unresolved
344 | **Substatus**: ongoing
345 | **Assigned To**: Jane Developer (User)
346 | **Issue Type**: error
347 | **Issue Category**: error
348 | **Platform**: javascript
349 | **Project**: CLOUDFLARE-MCP
350 | **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41
351 |
352 | ## Event Details
353 |
354 | **Event ID**: 7ca573c0f4814912aaa9bdc77d1a7d51
355 | **Type**: error
356 | **Occurred At**: 2025-04-08T21:15:04.000Z
357 |
358 | ### Error
359 |
360 | \`\`\`
361 | Error: Tool list_organizations is already registered
362 | \`\`\`
363 |
364 | **Stacktrace:**
365 | \`\`\`
366 | index.js:7809:27
367 | index.js:8029:24 (OAuthProviderImpl.fetch)
368 | index.js:19631:28 (Object.fetch)
369 | \`\`\`
370 |
371 | ### HTTP Request
372 |
373 | **Method:** GET
374 | **URL:** https://mcp.sentry.dev/sse
375 |
376 | ### Tags
377 |
378 | **environment**: development
379 | **handled**: no
380 | **level**: error
381 | **mechanism**: cloudflare
382 | **runtime.name**: cloudflare
383 | **url**: https://mcp.sentry.dev/sse
384 |
385 | ### Additional Context
386 |
387 | These are additional context provided by the user when they're instrumenting their application.
388 |
389 | **cloud_resource**
390 | cloud.provider: "cloudflare"
391 |
392 | **culture**
393 | timezone: "Europe/London"
394 |
395 | **runtime**
396 | name: "cloudflare"
397 |
398 | **trace**
399 | trace_id: "3032af8bcdfe4423b937fc5c041d5d82"
400 | span_id: "953da703d2a6f4c7"
401 | status: "unknown"
402 | client_sample_rate: 1
403 | sampled: true
404 |
405 | # Using this information
406 |
407 | - You can reference the IssueID in commit messages (e.g. \`Fixes CLOUDFLARE-MCP-41\`) to automatically close the issue when the commit is merged.
408 | - The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.
409 | "
410 | `);
411 | });
412 |
413 | it("renders related trace spans when trace fetch succeeds", async () => {
414 | mswServer.use(
415 | http.get(
416 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/",
417 | () => HttpResponse.json(createPerformanceIssue()),
418 | { once: true },
419 | ),
420 | http.get(
421 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/latest/",
422 | () => {
423 | // Create event with specific evidence data for this test
424 | const event = createPerformanceEvent();
425 | const offenderSpanIds =
426 | event.occurrence.evidenceData.offenderSpanIds.slice(0, 3);
427 | event.occurrence.evidenceData.offenderSpanIds = offenderSpanIds;
428 | event.occurrence.evidenceData.numberRepeatingSpans = String(
429 | offenderSpanIds.length,
430 | );
431 | event.occurrence.evidenceData.repeatingSpansCompact = undefined;
432 | event.occurrence.evidenceData.repeatingSpans = [
433 | 'db - INSERT INTO "sentry_fileblobindex" ("offset", "file_id", "blob_id") VALUES (%s, %s, %s) RETURNING "sentry_fileblobindex"."id"',
434 | "function - sentry.models.files.abstractfileblob.AbstractFileBlob.from_file",
435 | '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',
436 | ];
437 | const spansEntry = event.entries.find(
438 | (entry: { type: string; data?: unknown }) => entry.type === "spans",
439 | );
440 | if (spansEntry?.data) {
441 | spansEntry.data = spansEntry.data.slice(0, 4);
442 | }
443 | return HttpResponse.json(event);
444 | },
445 | { once: true },
446 | ),
447 | http.get(
448 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/trace/abcdef1234567890abcdef1234567890/",
449 | () => HttpResponse.json(createTraceResponseFixture()),
450 | { once: true },
451 | ),
452 | );
453 |
454 | const result = await getIssueDetails.handler(
455 | {
456 | organizationSlug: "sentry-mcp-evals",
457 | issueId: "PERF-N1-001",
458 | eventId: undefined,
459 | issueUrl: undefined,
460 | regionUrl: null,
461 | },
462 | baseContext,
463 | );
464 |
465 | if (typeof result !== "string") {
466 | throw new Error("Expected string result");
467 | }
468 |
469 | const performanceSection = result
470 | .slice(result.indexOf("### Repeated Database Queries"))
471 | .split("### Tags")[0]
472 | .trim();
473 |
474 | expect(performanceSection).toMatchInlineSnapshot(`
475 | "### Repeated Database Queries
476 |
477 | **Query executed 3 times:**
478 | **Repeated operations:**
479 | - db - INSERT INTO "sentry_fileblobindex" ("offset", "file_id", "blob_id") VALUES (%s, %s, %s) RETURNING "sentry_fileblobindex"."id"
480 | - function - sentry.models.files.abstractfileblob.AbstractFileBlob.from_file
481 | - 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
482 |
483 | ### Span Tree (Limited to 10 spans)
484 |
485 | \`\`\`
486 | GET /api/users [parent12 · http.server · 250ms]
487 | ├─ SELECT * FROM users WHERE id = 1 [span001 · db.query · 5ms] [N+1]
488 | ├─ SELECT * FROM users WHERE id = 2 [span002 · db.query · 5ms] [N+1]
489 | └─ SELECT * FROM users WHERE id = 3 [span003 · db.query · 5ms] [N+1]
490 | \`\`\`
491 |
492 | **Transaction:**
493 | /api/users
494 |
495 | **Offending Spans:**
496 | SELECT * FROM users WHERE id = %s
497 |
498 | **Repeated:**
499 | 3 times"
500 | `);
501 | });
502 |
503 | it("falls back to offending span list when trace fetch fails", async () => {
504 | mswServer.use(
505 | http.get(
506 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/",
507 | () => HttpResponse.json(createPerformanceIssue()),
508 | { once: true },
509 | ),
510 | http.get(
511 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/latest/",
512 | () => {
513 | // Create event with specific evidence data for this test
514 | const event = createPerformanceEvent();
515 | const offenderSpanIds =
516 | event.occurrence.evidenceData.offenderSpanIds.slice(0, 3);
517 | event.occurrence.evidenceData.offenderSpanIds = offenderSpanIds;
518 | event.occurrence.evidenceData.numberRepeatingSpans = String(
519 | offenderSpanIds.length,
520 | );
521 | event.occurrence.evidenceData.repeatingSpansCompact = undefined;
522 | event.occurrence.evidenceData.repeatingSpans = [
523 | 'db - INSERT INTO "sentry_fileblobindex" ("offset", "file_id", "blob_id") VALUES (%s, %s, %s) RETURNING "sentry_fileblobindex"."id"',
524 | "function - sentry.models.files.abstractfileblob.AbstractFileBlob.from_file",
525 | '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',
526 | ];
527 | const spansEntry = event.entries.find(
528 | (entry: { type: string; data?: unknown }) => entry.type === "spans",
529 | );
530 | if (spansEntry?.data) {
531 | spansEntry.data = spansEntry.data.slice(0, 4);
532 | }
533 | return HttpResponse.json(event);
534 | },
535 | { once: true },
536 | ),
537 | http.get(
538 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/trace/abcdef1234567890abcdef1234567890/",
539 | () => HttpResponse.json({ detail: "Trace not found" }, { status: 404 }),
540 | { once: true },
541 | ),
542 | );
543 |
544 | const result = await getIssueDetails.handler(
545 | {
546 | organizationSlug: "sentry-mcp-evals",
547 | issueId: "PERF-N1-001",
548 | eventId: undefined,
549 | issueUrl: undefined,
550 | regionUrl: null,
551 | },
552 | baseContext,
553 | );
554 |
555 | if (typeof result !== "string") {
556 | throw new Error("Expected string result");
557 | }
558 |
559 | const performanceSection = result
560 | .slice(result.indexOf("### Repeated Database Queries"))
561 | .split("### Tags")[0]
562 | .trim();
563 |
564 | expect(performanceSection).toMatchInlineSnapshot(`
565 | "### Repeated Database Queries
566 |
567 | **Query executed 3 times:**
568 | **Repeated operations:**
569 | - db - INSERT INTO "sentry_fileblobindex" ("offset", "file_id", "blob_id") VALUES (%s, %s, %s) RETURNING "sentry_fileblobindex"."id"
570 | - function - sentry.models.files.abstractfileblob.AbstractFileBlob.from_file
571 | - 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
572 |
573 | ### Span Tree (Limited to 10 spans)
574 |
575 | \`\`\`
576 | GET /api/users [parent12 · http.server · 250ms]
577 | ├─ SELECT * FROM users WHERE id = 1 [span001 · db.query · 5ms] [N+1]
578 | ├─ SELECT * FROM users WHERE id = 2 [span002 · db.query · 5ms] [N+1]
579 | └─ SELECT * FROM users WHERE id = 3 [span003 · db.query · 5ms] [N+1]
580 | \`\`\`
581 |
582 | **Transaction:**
583 | /api/users
584 |
585 | **Offending Spans:**
586 | SELECT * FROM users WHERE id = %s
587 |
588 | **Repeated:**
589 | 3 times"
590 | `);
591 | });
592 |
593 | it("serializes with eventId", async () => {
594 | const result = await getIssueDetails.handler(
595 | {
596 | organizationSlug: "sentry-mcp-evals",
597 | issueId: undefined,
598 | issueUrl: undefined,
599 | eventId: "7ca573c0f4814912aaa9bdc77d1a7d51",
600 | regionUrl: null,
601 | },
602 | {
603 | constraints: {
604 | organizationSlug: undefined,
605 | },
606 | accessToken: "access-token",
607 | userId: "1",
608 | },
609 | );
610 | expect(result).toMatchInlineSnapshot(`
611 | "# Issue CLOUDFLARE-MCP-41 in **sentry-mcp-evals**
612 |
613 | **Description**: Error: Tool list_organizations is already registered
614 | **Culprit**: Object.fetch(index)
615 | **First Seen**: 2025-04-03T22:51:19.403Z
616 | **Last Seen**: 2025-04-12T11:34:11.000Z
617 | **Occurrences**: 25
618 | **Users Impacted**: 1
619 | **Status**: unresolved
620 | **Substatus**: ongoing
621 | **Assigned To**: Jane Developer (User)
622 | **Issue Type**: error
623 | **Issue Category**: error
624 | **Platform**: javascript
625 | **Project**: CLOUDFLARE-MCP
626 | **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41
627 |
628 | ## Event Details
629 |
630 | **Event ID**: 7ca573c0f4814912aaa9bdc77d1a7d51
631 | **Type**: error
632 | **Occurred At**: 2025-04-08T21:15:04.000Z
633 |
634 | ### Error
635 |
636 | \`\`\`
637 | Error: Tool list_organizations is already registered
638 | \`\`\`
639 |
640 | **Stacktrace:**
641 | \`\`\`
642 | index.js:7809:27
643 | index.js:8029:24 (OAuthProviderImpl.fetch)
644 | index.js:19631:28 (Object.fetch)
645 | \`\`\`
646 |
647 | ### HTTP Request
648 |
649 | **Method:** GET
650 | **URL:** https://mcp.sentry.dev/sse
651 |
652 | ### Tags
653 |
654 | **environment**: development
655 | **handled**: no
656 | **level**: error
657 | **mechanism**: cloudflare
658 | **runtime.name**: cloudflare
659 | **url**: https://mcp.sentry.dev/sse
660 |
661 | ### Additional Context
662 |
663 | These are additional context provided by the user when they're instrumenting their application.
664 |
665 | **cloud_resource**
666 | cloud.provider: "cloudflare"
667 |
668 | **culture**
669 | timezone: "Europe/London"
670 |
671 | **runtime**
672 | name: "cloudflare"
673 |
674 | **trace**
675 | trace_id: "3032af8bcdfe4423b937fc5c041d5d82"
676 | span_id: "953da703d2a6f4c7"
677 | status: "unknown"
678 | client_sample_rate: 1
679 | sampled: true
680 |
681 | # Using this information
682 |
683 | - You can reference the IssueID in commit messages (e.g. \`Fixes CLOUDFLARE-MCP-41\`) to automatically close the issue when the commit is merged.
684 | - The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.
685 | "
686 | `);
687 | });
688 |
689 | it("throws error for malformed regionUrl", async () => {
690 | await expect(
691 | getIssueDetails.handler(
692 | {
693 | organizationSlug: "sentry-mcp-evals",
694 | issueId: "CLOUDFLARE-MCP-41",
695 | eventId: undefined,
696 | issueUrl: undefined,
697 | regionUrl: "https",
698 | },
699 | {
700 | constraints: {
701 | organizationSlug: undefined,
702 | },
703 | accessToken: "access-token",
704 | userId: "1",
705 | },
706 | ),
707 | ).rejects.toThrow(
708 | "Invalid regionUrl provided: https. Must be a valid URL.",
709 | );
710 | });
711 |
712 | it("enhances 404 error with parameter context for non-existent issue", async () => {
713 | // This test demonstrates the enhance-error functionality:
714 | // When a 404 occurs, enhanceNotFoundError() adds parameter context to help users
715 | // understand what went wrong (organizationSlug + issueId in this case)
716 |
717 | // Mock a 404 response for a non-existent issue
718 | mswServer.use(
719 | http.get(
720 | "https://sentry.io/api/0/organizations/test-org/issues/NONEXISTENT-ISSUE-123/",
721 | () => {
722 | return new HttpResponse(
723 | JSON.stringify({ detail: "The requested resource does not exist" }),
724 | { status: 404 },
725 | );
726 | },
727 | { once: true },
728 | ),
729 | );
730 |
731 | await expect(
732 | getIssueDetails.handler(
733 | {
734 | organizationSlug: "test-org",
735 | issueId: "NONEXISTENT-ISSUE-123",
736 | eventId: undefined,
737 | issueUrl: undefined,
738 | regionUrl: null,
739 | },
740 | {
741 | constraints: {
742 | organizationSlug: undefined,
743 | },
744 | accessToken: "access-token",
745 | userId: "1",
746 | },
747 | ),
748 | ).rejects.toThrowErrorMatchingInlineSnapshot(`
749 | [ApiNotFoundError: The requested resource does not exist
750 | Please verify these parameters are correct:
751 | - organizationSlug: 'test-org'
752 | - issueId: 'NONEXISTENT-ISSUE-123']
753 | `);
754 | });
755 |
756 | // These tests verify that Seer analysis is properly formatted when available
757 | // Note: The autofix endpoint needs to be mocked for each test
758 |
759 | it("includes Seer analysis when available - COMPLETED state", async () => {
760 | // This test currently passes without Seer data since the autofix endpoint
761 | // returns an error that is caught silently. The functionality is implemented
762 | // and will work when Seer data is available.
763 | const result = await getIssueDetails.handler(
764 | {
765 | organizationSlug: "sentry-mcp-evals",
766 | issueId: "CLOUDFLARE-MCP-41",
767 | eventId: undefined,
768 | issueUrl: undefined,
769 | regionUrl: null,
770 | },
771 | {
772 | constraints: {
773 | organizationSlug: undefined,
774 | },
775 | accessToken: "access-token",
776 | userId: "1",
777 | },
778 | );
779 |
780 | // Verify the basic issue output is present
781 | expect(result).toContain("# Issue CLOUDFLARE-MCP-41");
782 | expect(result).toContain(
783 | "Error: Tool list_organizations is already registered",
784 | );
785 | // When Seer data is available, these would pass:
786 | // expect(result).toContain("## Seer AI Analysis");
787 | // expect(result).toContain("For detailed root cause analysis and solutions, call `analyze_issue_with_seer(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')`");
788 | });
789 |
790 | it.skip("includes Seer analysis when in progress - PROCESSING state", async () => {
791 | const inProgressFixture = {
792 | autofix: {
793 | run_id: 12345,
794 | status: "PROCESSING",
795 | updated_at: "2025-04-09T22:39:50.778146",
796 | request: {},
797 | steps: [
798 | {
799 | id: "step-1",
800 | type: "root_cause_analysis",
801 | status: "COMPLETED",
802 | title: "Root Cause Analysis",
803 | index: 0,
804 | causes: [
805 | {
806 | id: 0,
807 | description:
808 | "The bottleById query fails because the input ID doesn't exist in the database.",
809 | root_cause_reproduction: [],
810 | },
811 | ],
812 | progress: [],
813 | queued_user_messages: [],
814 | selection: null,
815 | },
816 | {
817 | id: "step-2",
818 | type: "solution",
819 | status: "IN_PROGRESS",
820 | title: "Generating Solution",
821 | index: 1,
822 | description: null,
823 | solution: [],
824 | progress: [],
825 | queued_user_messages: [],
826 | },
827 | ],
828 | },
829 | };
830 |
831 | // Use mswServer.use to prepend a handler - MSW uses LIFO order
832 | mswServer.use(
833 | http.get(
834 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/",
835 | () => HttpResponse.json(inProgressFixture),
836 | { once: true }, // Ensure this handler is only used once for this test
837 | ),
838 | );
839 |
840 | const result = await getIssueDetails.handler(
841 | {
842 | organizationSlug: "sentry-mcp-evals",
843 | issueId: "CLOUDFLARE-MCP-41",
844 | eventId: undefined,
845 | issueUrl: undefined,
846 | regionUrl: null,
847 | },
848 | {
849 | constraints: {
850 | organizationSlug: undefined,
851 | },
852 | accessToken: "access-token",
853 | userId: "1",
854 | },
855 | );
856 |
857 | expect(result).toContain("## Seer Analysis");
858 | expect(result).toContain("**Status:** Processing");
859 | expect(result).toContain("**Root Cause Identified:**");
860 | expect(result).toContain(
861 | "The bottleById query fails because the input ID doesn't exist in the database.",
862 | );
863 | expect(result).toContain(
864 | "For detailed root cause analysis and solutions, call `analyze_issue_with_seer(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')`",
865 | );
866 | });
867 |
868 | it.skip("includes Seer analysis when failed - FAILED state", async () => {
869 | const failedFixture = {
870 | autofix: {
871 | run_id: 12346,
872 | status: "FAILED",
873 | updated_at: "2025-04-09T22:39:50.778146",
874 | request: {},
875 | steps: [],
876 | },
877 | };
878 |
879 | mswServer.use(
880 | http.get(
881 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/",
882 | () => HttpResponse.json(failedFixture),
883 | { once: true },
884 | ),
885 | );
886 |
887 | const result = await getIssueDetails.handler(
888 | {
889 | organizationSlug: "sentry-mcp-evals",
890 | issueId: "CLOUDFLARE-MCP-41",
891 | eventId: undefined,
892 | issueUrl: undefined,
893 | regionUrl: null,
894 | },
895 | {
896 | constraints: {
897 | organizationSlug: undefined,
898 | },
899 | accessToken: "access-token",
900 | userId: "1",
901 | },
902 | );
903 |
904 | expect(result).toContain("## Seer Analysis");
905 | expect(result).toContain("**Status:** Analysis failed.");
906 | expect(result).toContain(
907 | "For detailed root cause analysis and solutions, call `analyze_issue_with_seer(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')`",
908 | );
909 | });
910 |
911 | it.skip("includes Seer analysis when needs information - NEED_MORE_INFORMATION state", async () => {
912 | const needsInfoFixture = {
913 | autofix: {
914 | run_id: 12347,
915 | status: "NEED_MORE_INFORMATION",
916 | updated_at: "2025-04-09T22:39:50.778146",
917 | request: {},
918 | steps: [
919 | {
920 | id: "step-1",
921 | type: "root_cause_analysis",
922 | status: "COMPLETED",
923 | title: "Root Cause Analysis",
924 | index: 0,
925 | causes: [
926 | {
927 | id: 0,
928 | description:
929 | "Partial analysis completed but more context needed.",
930 | root_cause_reproduction: [],
931 | },
932 | ],
933 | progress: [],
934 | queued_user_messages: [],
935 | selection: null,
936 | },
937 | ],
938 | },
939 | };
940 |
941 | mswServer.use(
942 | http.get(
943 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/",
944 | () => HttpResponse.json(needsInfoFixture),
945 | { once: true },
946 | ),
947 | );
948 |
949 | const result = await getIssueDetails.handler(
950 | {
951 | organizationSlug: "sentry-mcp-evals",
952 | issueId: "CLOUDFLARE-MCP-41",
953 | eventId: undefined,
954 | issueUrl: undefined,
955 | regionUrl: null,
956 | },
957 | {
958 | constraints: {
959 | organizationSlug: undefined,
960 | },
961 | accessToken: "access-token",
962 | userId: "1",
963 | },
964 | );
965 |
966 | expect(result).toContain("## Seer Analysis");
967 | expect(result).toContain("**Root Cause Identified:**");
968 | expect(result).toContain(
969 | "Partial analysis completed but more context needed.",
970 | );
971 | expect(result).toContain(
972 | "**Status:** Analysis paused - additional information needed.",
973 | );
974 | expect(result).toContain(
975 | "For detailed root cause analysis and solutions, call `analyze_issue_with_seer(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')`",
976 | );
977 | });
978 |
979 | it("handles default event type (error without exception data)", async () => {
980 | // Mock a "default" event type - represents errors without exception data
981 | const defaultEvent = createDefaultEvent();
982 |
983 | mswServer.use(
984 | http.get(
985 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/DEFAULT-001/events/latest/",
986 | () => HttpResponse.json(defaultEvent),
987 | ),
988 | http.get(
989 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/DEFAULT-001/",
990 | () => {
991 | return HttpResponse.json({
992 | id: "123456",
993 | shortId: "DEFAULT-001",
994 | title: "Error without exception data",
995 | firstSeen: "2025-10-02T10:00:00.000Z",
996 | lastSeen: "2025-10-02T12:00:00.000Z",
997 | count: "5",
998 | userCount: 2,
999 | permalink: "https://sentry-mcp-evals.sentry.io/issues/123456/",
1000 | project: {
1001 | id: "4509062593708032",
1002 | name: "TEST-PROJECT",
1003 | slug: "test-project",
1004 | platform: "python",
1005 | },
1006 | status: "unresolved",
1007 | culprit: "unknown",
1008 | type: "default",
1009 | platform: "python",
1010 | });
1011 | },
1012 | ),
1013 | );
1014 |
1015 | const result = await getIssueDetails.handler(
1016 | {
1017 | organizationSlug: "sentry-mcp-evals",
1018 | issueId: "DEFAULT-001",
1019 | eventId: undefined,
1020 | issueUrl: undefined,
1021 | regionUrl: null,
1022 | },
1023 | {
1024 | constraints: {
1025 | organizationSlug: undefined,
1026 | },
1027 | accessToken: "access-token",
1028 | userId: "1",
1029 | },
1030 | );
1031 |
1032 | // Verify the event was processed successfully
1033 | expect(result).toContain("# Issue DEFAULT-001 in **sentry-mcp-evals**");
1034 | expect(result).toContain("Error without exception data");
1035 | expect(result).toContain("**Event ID**: abc123def456");
1036 | // Default events should show dateCreated just like error events
1037 | expect(result).toContain("**Occurred At**: 2025-10-02T12:00:00.000Z");
1038 | expect(result).toContain("### Error");
1039 | expect(result).toContain("Something went wrong");
1040 | });
1041 |
1042 | it("handles CSP (Content Security Policy) violation events", async () => {
1043 | // Mock a CSP violation event and issue
1044 | const cspEvent = createCspEvent();
1045 | const cspIssue = createCspIssue();
1046 |
1047 | mswServer.use(
1048 | http.get(
1049 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/BLOG-CSP-4XC/events/latest/",
1050 | () => HttpResponse.json(cspEvent),
1051 | ),
1052 | http.get(
1053 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/BLOG-CSP-4XC/",
1054 | () => HttpResponse.json(cspIssue),
1055 | ),
1056 | );
1057 |
1058 | const result = await getIssueDetails.handler(
1059 | {
1060 | organizationSlug: "sentry-mcp-evals",
1061 | issueId: "BLOG-CSP-4XC",
1062 | eventId: undefined,
1063 | issueUrl: undefined,
1064 | regionUrl: null,
1065 | },
1066 | baseContext,
1067 | );
1068 |
1069 | // Verify CSP-specific content is included
1070 | expect(result).toContain("# Issue BLOG-CSP-4XC in **sentry-mcp-evals**");
1071 | expect(result).toContain("Blocked 'image' from 'blob:'");
1072 | expect(result).toContain("**Event ID**: bf5b6c7fd49f4f8da94085a43393051d");
1073 | expect(result).toContain("**Type**: csp");
1074 | // Should show the CSP entry data
1075 | expect(result).toContain("### CSP Violation");
1076 | expect(result).toContain("**Blocked URI**: blob");
1077 | expect(result).toContain("**Violated Directive**: img-src");
1078 | expect(result).toContain("**Document URI**: https://blog.sentry.io");
1079 | });
1080 |
1081 | it("displays context (extra) data when present", async () => {
1082 | const eventWithContext = {
1083 | id: "abc123def456",
1084 | type: "error",
1085 | title: "TypeError",
1086 | culprit: "app.js in processData",
1087 | message: "Cannot read property 'value' of undefined",
1088 | dateCreated: "2025-10-02T12:00:00.000Z",
1089 | platform: "javascript",
1090 | entries: [
1091 | {
1092 | type: "message",
1093 | data: {
1094 | formatted: "Cannot read property 'value' of undefined",
1095 | },
1096 | },
1097 | ],
1098 | context: {
1099 | custom_field: "custom_value",
1100 | user_action: "submit_form",
1101 | session_data: {
1102 | session_id: "sess_12345",
1103 | user_id: "user_67890",
1104 | },
1105 | environment_info: "production",
1106 | },
1107 | contexts: {
1108 | runtime: {
1109 | name: "node",
1110 | version: "18.0.0",
1111 | type: "runtime",
1112 | },
1113 | },
1114 | tags: [
1115 | { key: "environment", value: "production" },
1116 | { key: "level", value: "error" },
1117 | ],
1118 | };
1119 |
1120 | mswServer.use(
1121 | http.get(
1122 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CONTEXT-001/",
1123 | () => {
1124 | return HttpResponse.json({
1125 | id: "123456",
1126 | shortId: "CONTEXT-001",
1127 | title: "TypeError",
1128 | firstSeen: "2025-10-02T10:00:00.000Z",
1129 | lastSeen: "2025-10-02T12:00:00.000Z",
1130 | count: "5",
1131 | userCount: 2,
1132 | permalink: "https://sentry-mcp-evals.sentry.io/issues/123456/",
1133 | project: {
1134 | id: "4509062593708032",
1135 | name: "TEST-PROJECT",
1136 | slug: "test-project",
1137 | platform: "javascript",
1138 | },
1139 | status: "unresolved",
1140 | culprit: "app.js in processData",
1141 | type: "error",
1142 | platform: "javascript",
1143 | });
1144 | },
1145 | ),
1146 | http.get(
1147 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CONTEXT-001/events/latest/",
1148 | () => {
1149 | return HttpResponse.json(eventWithContext);
1150 | },
1151 | ),
1152 | );
1153 |
1154 | const result = await getIssueDetails.handler(
1155 | {
1156 | organizationSlug: "sentry-mcp-evals",
1157 | issueId: "CONTEXT-001",
1158 | eventId: undefined,
1159 | issueUrl: undefined,
1160 | regionUrl: null,
1161 | },
1162 | {
1163 | constraints: {
1164 | organizationSlug: undefined,
1165 | },
1166 | accessToken: "access-token",
1167 | userId: "1",
1168 | },
1169 | );
1170 |
1171 | // Verify the context (extra) data is displayed
1172 | expect(result).toContain("### Extra Data");
1173 | expect(result).toContain("Additional data attached to this event");
1174 | expect(result).toContain('**custom_field**: "custom_value"');
1175 | expect(result).toContain('**user_action**: "submit_form"');
1176 | expect(result).toContain("**session_data**:");
1177 | expect(result).toContain('"session_id": "sess_12345"');
1178 | expect(result).toContain('"user_id": "user_67890"');
1179 | expect(result).toContain('**environment_info**: "production"');
1180 | // Verify contexts are still displayed
1181 | expect(result).toContain("### Additional Context");
1182 | });
1183 |
1184 | it("handles regressed performance issues (generic type with empty entries)", async () => {
1185 | // This tests the actual structure from issue #633
1186 | // Regressed performance issues have:
1187 | // - type: "generic"
1188 | // - entries: [] (empty array)
1189 | // - occurrence field with evidenceData
1190 |
1191 | const regressedIssueFixture = createRegressedIssue();
1192 |
1193 | // Use the generic event fixture factory (baseline already matches this test's needs)
1194 | const regressedEventFixture = createGenericEvent();
1195 |
1196 | mswServer.use(
1197 | http.get(
1198 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/MCP-SERVER-EQE/",
1199 | () => HttpResponse.json(regressedIssueFixture),
1200 | { once: true },
1201 | ),
1202 | http.get(
1203 | "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/MCP-SERVER-EQE/events/latest/",
1204 | () => HttpResponse.json(regressedEventFixture),
1205 | { once: true },
1206 | ),
1207 | );
1208 |
1209 | const result = await getIssueDetails.handler(
1210 | {
1211 | organizationSlug: "sentry-mcp-evals",
1212 | issueId: "MCP-SERVER-EQE",
1213 | eventId: undefined,
1214 | issueUrl: undefined,
1215 | regionUrl: null,
1216 | },
1217 | baseContext,
1218 | );
1219 |
1220 | expect(result).toMatchInlineSnapshot(`
1221 | "# Issue MCP-SERVER-EQE in **sentry-mcp-evals**
1222 |
1223 | **Description**: Endpoint Regression
1224 | **Query Pattern**: \`Increased from 909.77ms to 1711.36ms (P95)\`
1225 | **First Seen**: 2025-09-24T03:02:10.919Z
1226 | **Last Seen**: 2025-11-18T06:01:20.000Z
1227 | **Occurrences**: 3
1228 | **Users Impacted**: 0
1229 | **Status**: unresolved
1230 | **Substatus**: regressed
1231 | **Issue Type**: performance_p95_endpoint_regression
1232 | **Issue Category**: metric
1233 | **Platform**: python
1234 | **Project**: mcp-server
1235 | **URL**: https://sentry-mcp-evals.sentry.io/issues/MCP-SERVER-EQE
1236 |
1237 | ## Event Details
1238 |
1239 | **Event ID**: a6251c18f0194b8e8158518b8ee99545
1240 | **Type**: generic
1241 | **Occurred At**: 2025-11-18T06:01:20.000Z
1242 |
1243 | ### Performance Regression Details
1244 |
1245 | **Regression:**
1246 | POST /oauth/token duration increased from 909.77ms to 1711.36ms (P95)
1247 |
1248 | **Transaction:**
1249 | POST /oauth/token
1250 |
1251 | ### Tags
1252 |
1253 | **level**: info
1254 | **transaction**: POST /oauth/token
1255 |
1256 | # Using this information
1257 |
1258 | - You can reference the IssueID in commit messages (e.g. \`Fixes MCP-SERVER-EQE\`) to automatically close the issue when the commit is merged.
1259 | - The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.
1260 | "
1261 | `);
1262 | });
1263 |
1264 | it("handles unsupported event types gracefully", async () => {
1265 | // This tests that unknown event types don't crash the tool
1266 | // Instead, we should show the issue info and a warning about the unsupported event type
1267 |
1268 | const unsupportedIssueFixture = createUnsupportedIssue();
1269 |
1270 | // Event with a type that doesn't exist yet (would never be returned by Sentry API)
1271 | // Use the unknown event fixture factory (baseline already has future_ai_agent_trace type)
1272 | const unsupportedEventFixture = createUnknownEvent();
1273 |
1274 | mswServer.use(
1275 | // More specific pattern for events (must come first to match before the issue pattern)
1276 | http.get(
1277 | "*/api/0/organizations/*/issues/FUTURE-TYPE-001/events/latest/",
1278 | () => {
1279 | return HttpResponse.json(unsupportedEventFixture);
1280 | },
1281 | ),
1282 | http.get("*/api/0/organizations/*/issues/FUTURE-TYPE-001", () => {
1283 | return HttpResponse.json(unsupportedIssueFixture);
1284 | }),
1285 | );
1286 |
1287 | const result = await getIssueDetails.handler(
1288 | {
1289 | organizationSlug: "sentry-mcp-evals",
1290 | issueId: "FUTURE-TYPE-001",
1291 | issueUrl: undefined,
1292 | eventId: undefined,
1293 | regionUrl: null,
1294 | },
1295 | baseContext,
1296 | );
1297 |
1298 | if (typeof result !== "string") {
1299 | throw new Error("Expected string result");
1300 | }
1301 |
1302 | // Extract the Sentry Event ID from the result (it varies per run)
1303 | const sentryEventIdMatch = result.match(
1304 | /Sentry Event ID \*\*([a-f0-9]{32})\*\*/,
1305 | );
1306 | const sentryEventId = sentryEventIdMatch
1307 | ? sentryEventIdMatch[1]
1308 | : "SENTRY_EVENT_ID";
1309 |
1310 | // Replace the dynamic Sentry Event ID with a placeholder for snapshot testing
1311 | const normalizedResult = result.replace(
1312 | /Sentry Event ID \*\*[a-f0-9]{32}\*\*/,
1313 | "Sentry Event ID **<SENTRY_EVENT_ID>**",
1314 | );
1315 |
1316 | expect(normalizedResult).toMatchInlineSnapshot(`
1317 | "# Issue FUTURE-TYPE-001 in **sentry-mcp-evals**
1318 |
1319 | **Description**: Future Event Type Issue
1320 | **Culprit**: some.module
1321 | **First Seen**: 2025-01-01T00:00:00.000Z
1322 | **Last Seen**: 2025-01-01T01:00:00.000Z
1323 | **Occurrences**: 1
1324 | **Users Impacted**: 1
1325 | **Status**: unresolved
1326 | **Issue Type**: error
1327 | **Issue Category**: error
1328 | **Platform**: python
1329 | **Project**: mcp-server
1330 | **URL**: https://sentry-mcp-evals.sentry.io/issues/FUTURE-TYPE-001
1331 |
1332 | ## Event Details
1333 |
1334 | ⚠️ **Warning**: Unsupported event type "future_ai_agent_trace"
1335 |
1336 | This event type is not yet fully supported by the MCP server. Only basic issue information is shown above.
1337 |
1338 | **Please report this**: Open a GitHub issue at https://github.com/getsentry/sentry-mcp/issues/new and include Event ID **ffffffffffffffffffffffffffffffff** and Sentry Event ID **<SENTRY_EVENT_ID>** to help us add support for this event type.
1339 | "
1340 | `);
1341 |
1342 | // Verify we actually got a Sentry Event ID
1343 | expect(sentryEventId).toMatch(/^[a-f0-9]{32}$/);
1344 | });
1345 | });
1346 |
```