#
tokens: 42530/50000 4/419 files (page 9/12)
lines: off (toggle) GitHub
raw markdown copy
This is page 9 of 12. Use http://codebase.md/getsentry/sentry-mcp?lines=false&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
├── .gitignore
├── .mcp.json
├── .vscode
│   ├── extensions.json
│   ├── mcp.json
│   └── settings.json
├── AGENTS.md
├── bin
│   └── bump-version.sh
├── biome.json
├── CLAUDE.md
├── codecov.yml
├── core
├── docs
│   ├── adding-tools.mdc
│   ├── api-patterns.mdc
│   ├── architecture.mdc
│   ├── cloudflare
│   │   ├── architecture.md
│   │   ├── deployment.md
│   │   ├── oauth-architecture.md
│   │   └── overview.md
│   ├── coding-guidelines.mdc
│   ├── common-patterns.mdc
│   ├── cursor.mdc
│   ├── deployment.mdc
│   ├── error-handling.mdc
│   ├── github-actions.mdc
│   ├── llms
│   │   ├── document-scopes.mdc
│   │   ├── documentation-style-guide.mdc
│   │   └── README.md
│   ├── logging.mdc
│   ├── monitoring.mdc
│   ├── permissions-and-scopes.md
│   ├── pr-management.mdc
│   ├── quality-checks.mdc
│   ├── README.md
│   ├── search-events-api-patterns.md
│   ├── security.mdc
│   ├── specs
│   │   ├── README.md
│   │   ├── search-events.md
│   │   └── subpath-constraints.md
│   └── testing.mdc
├── LICENSE.md
├── Makefile
├── package.json
├── packages
│   ├── mcp-cloudflare
│   │   ├── .env.example
│   │   ├── components.json
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── public
│   │   │   ├── favicon.ico
│   │   │   ├── flow-transparent.png
│   │   │   └── flow.jpg
│   │   ├── src
│   │   │   ├── client
│   │   │   │   ├── app.tsx
│   │   │   │   ├── components
│   │   │   │   │   ├── chat
│   │   │   │   │   │   ├── auth-form.tsx
│   │   │   │   │   │   ├── chat-input.tsx
│   │   │   │   │   │   ├── chat-message.tsx
│   │   │   │   │   │   ├── chat-messages.tsx
│   │   │   │   │   │   ├── chat-ui.tsx
│   │   │   │   │   │   ├── chat.tsx
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── tool-invocation.tsx
│   │   │   │   │   │   └── types.ts
│   │   │   │   │   ├── fragments
│   │   │   │   │   │   ├── remote-setup.tsx
│   │   │   │   │   │   ├── setup-guide.tsx
│   │   │   │   │   │   └── stdio-setup.tsx
│   │   │   │   │   └── ui
│   │   │   │   │       ├── accordion.tsx
│   │   │   │   │       ├── backdrop.tsx
│   │   │   │   │       ├── badge.tsx
│   │   │   │   │       ├── base.tsx
│   │   │   │   │       ├── button.tsx
│   │   │   │   │       ├── code-snippet.tsx
│   │   │   │   │       ├── header.tsx
│   │   │   │   │       ├── icon.tsx
│   │   │   │   │       ├── icons
│   │   │   │   │       │   └── sentry.tsx
│   │   │   │   │       ├── interactive-markdown.tsx
│   │   │   │   │       ├── json-schema-params.tsx
│   │   │   │   │       ├── markdown.tsx
│   │   │   │   │       ├── note.tsx
│   │   │   │   │       ├── prose.tsx
│   │   │   │   │       ├── section.tsx
│   │   │   │   │       ├── slash-command-actions.tsx
│   │   │   │   │       ├── slash-command-text.tsx
│   │   │   │   │       ├── sliding-panel.tsx
│   │   │   │   │       ├── template-vars.tsx
│   │   │   │   │       ├── tool-actions.tsx
│   │   │   │   │       └── typewriter.tsx
│   │   │   │   ├── contexts
│   │   │   │   │   └── auth-context.tsx
│   │   │   │   ├── hooks
│   │   │   │   │   ├── use-endpoint-mode.ts
│   │   │   │   │   ├── use-mcp-metadata.ts
│   │   │   │   │   ├── use-persisted-chat.ts
│   │   │   │   │   ├── use-scroll-lock.ts
│   │   │   │   │   └── use-streaming-simulation.ts
│   │   │   │   ├── index.css
│   │   │   │   ├── instrument.ts
│   │   │   │   ├── lib
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── main.tsx
│   │   │   │   ├── pages
│   │   │   │   │   └── home.tsx
│   │   │   │   ├── utils
│   │   │   │   │   ├── chat-error-handler.ts
│   │   │   │   │   └── index.ts
│   │   │   │   └── vite-env.d.ts
│   │   │   ├── constants.ts
│   │   │   ├── server
│   │   │   │   ├── app.test.ts
│   │   │   │   ├── app.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── lib
│   │   │   │   │   ├── approval-dialog.test.ts
│   │   │   │   │   ├── approval-dialog.ts
│   │   │   │   │   ├── constraint-utils.test.ts
│   │   │   │   │   ├── constraint-utils.ts
│   │   │   │   │   ├── html-utils.ts
│   │   │   │   │   ├── mcp-handler.test.ts
│   │   │   │   │   ├── mcp-handler.ts
│   │   │   │   │   └── slug-validation.ts
│   │   │   │   ├── logging.ts
│   │   │   │   ├── oauth
│   │   │   │   │   ├── authorize.test.ts
│   │   │   │   │   ├── callback.test.ts
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── helpers.test.ts
│   │   │   │   │   ├── helpers.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── authorize.ts
│   │   │   │   │   │   ├── callback.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   └── state.ts
│   │   │   │   ├── routes
│   │   │   │   │   ├── chat-oauth.ts
│   │   │   │   │   ├── chat.ts
│   │   │   │   │   ├── mcp.ts
│   │   │   │   │   ├── metadata.ts
│   │   │   │   │   ├── search.test.ts
│   │   │   │   │   └── search.ts
│   │   │   │   ├── sentry.config.ts
│   │   │   │   ├── types
│   │   │   │   │   └── chat.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── utils
│   │   │   │       └── auth-errors.ts
│   │   │   └── test-setup.ts
│   │   ├── tsconfig.client.json
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   ├── tsconfig.server.json
│   │   ├── vite.config.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   ├── wrangler.canary.jsonc
│   │   └── wrangler.jsonc
│   ├── mcp-server
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── scripts
│   │   │   ├── generate-definitions.ts
│   │   │   └── generate-otel-namespaces.ts
│   │   ├── src
│   │   │   ├── api-client
│   │   │   │   ├── client.test.ts
│   │   │   │   ├── client.ts
│   │   │   │   ├── errors.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── schema.ts
│   │   │   │   └── types.ts
│   │   │   ├── cli
│   │   │   │   ├── parse.test.ts
│   │   │   │   ├── parse.ts
│   │   │   │   ├── resolve.test.ts
│   │   │   │   ├── resolve.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── usage.ts
│   │   │   ├── constants.ts
│   │   │   ├── errors.test.ts
│   │   │   ├── errors.ts
│   │   │   ├── index.ts
│   │   │   ├── internal
│   │   │   │   ├── agents
│   │   │   │   │   ├── callEmbeddedAgent.ts
│   │   │   │   │   ├── openai-provider.ts
│   │   │   │   │   └── tools
│   │   │   │   │       ├── data
│   │   │   │   │       │   ├── __namespaces.json
│   │   │   │   │       │   ├── android.json
│   │   │   │   │       │   ├── app.json
│   │   │   │   │       │   ├── artifact.json
│   │   │   │   │       │   ├── aspnetcore.json
│   │   │   │   │       │   ├── aws.json
│   │   │   │   │       │   ├── azure.json
│   │   │   │   │       │   ├── browser.json
│   │   │   │   │       │   ├── cassandra.json
│   │   │   │   │       │   ├── cicd.json
│   │   │   │   │       │   ├── CLAUDE.md
│   │   │   │   │       │   ├── client.json
│   │   │   │   │       │   ├── cloud.json
│   │   │   │   │       │   ├── cloudevents.json
│   │   │   │   │       │   ├── cloudfoundry.json
│   │   │   │   │       │   ├── code.json
│   │   │   │   │       │   ├── container.json
│   │   │   │   │       │   ├── cpu.json
│   │   │   │   │       │   ├── cpython.json
│   │   │   │   │       │   ├── database.json
│   │   │   │   │       │   ├── db.json
│   │   │   │   │       │   ├── deployment.json
│   │   │   │   │       │   ├── destination.json
│   │   │   │   │       │   ├── device.json
│   │   │   │   │       │   ├── disk.json
│   │   │   │   │       │   ├── dns.json
│   │   │   │   │       │   ├── dotnet.json
│   │   │   │   │       │   ├── elasticsearch.json
│   │   │   │   │       │   ├── enduser.json
│   │   │   │   │       │   ├── error.json
│   │   │   │   │       │   ├── faas.json
│   │   │   │   │       │   ├── feature_flags.json
│   │   │   │   │       │   ├── file.json
│   │   │   │   │       │   ├── gcp.json
│   │   │   │   │       │   ├── gen_ai.json
│   │   │   │   │       │   ├── geo.json
│   │   │   │   │       │   ├── go.json
│   │   │   │   │       │   ├── graphql.json
│   │   │   │   │       │   ├── hardware.json
│   │   │   │   │       │   ├── heroku.json
│   │   │   │   │       │   ├── host.json
│   │   │   │   │       │   ├── http.json
│   │   │   │   │       │   ├── ios.json
│   │   │   │   │       │   ├── jvm.json
│   │   │   │   │       │   ├── k8s.json
│   │   │   │   │       │   ├── linux.json
│   │   │   │   │       │   ├── log.json
│   │   │   │   │       │   ├── mcp.json
│   │   │   │   │       │   ├── messaging.json
│   │   │   │   │       │   ├── network.json
│   │   │   │   │       │   ├── nodejs.json
│   │   │   │   │       │   ├── oci.json
│   │   │   │   │       │   ├── opentracing.json
│   │   │   │   │       │   ├── os.json
│   │   │   │   │       │   ├── otel.json
│   │   │   │   │       │   ├── peer.json
│   │   │   │   │       │   ├── process.json
│   │   │   │   │       │   ├── profile.json
│   │   │   │   │       │   ├── rpc.json
│   │   │   │   │       │   ├── server.json
│   │   │   │   │       │   ├── service.json
│   │   │   │   │       │   ├── session.json
│   │   │   │   │       │   ├── signalr.json
│   │   │   │   │       │   ├── source.json
│   │   │   │   │       │   ├── system.json
│   │   │   │   │       │   ├── telemetry.json
│   │   │   │   │       │   ├── test.json
│   │   │   │   │       │   ├── thread.json
│   │   │   │   │       │   ├── tls.json
│   │   │   │   │       │   ├── url.json
│   │   │   │   │       │   ├── user.json
│   │   │   │   │       │   ├── v8js.json
│   │   │   │   │       │   ├── vcs.json
│   │   │   │   │       │   ├── webengine.json
│   │   │   │   │       │   └── zos.json
│   │   │   │   │       ├── dataset-fields.test.ts
│   │   │   │   │       ├── dataset-fields.ts
│   │   │   │   │       ├── otel-semantics.test.ts
│   │   │   │   │       ├── otel-semantics.ts
│   │   │   │   │       ├── utils.ts
│   │   │   │   │       ├── whoami.test.ts
│   │   │   │   │       └── whoami.ts
│   │   │   │   ├── constraint-helpers.test.ts
│   │   │   │   ├── constraint-helpers.ts
│   │   │   │   ├── context-storage.ts
│   │   │   │   ├── error-handling.ts
│   │   │   │   ├── fetch-utils.test.ts
│   │   │   │   ├── fetch-utils.ts
│   │   │   │   ├── formatting.test.ts
│   │   │   │   ├── formatting.ts
│   │   │   │   ├── issue-helpers.test.ts
│   │   │   │   ├── issue-helpers.ts
│   │   │   │   ├── test-fixtures.ts
│   │   │   │   └── tool-helpers
│   │   │   │       ├── api.test.ts
│   │   │   │       ├── api.ts
│   │   │   │       ├── define.ts
│   │   │   │       ├── enhance-error.ts
│   │   │   │       ├── formatting.ts
│   │   │   │       ├── issue.ts
│   │   │   │       ├── seer.test.ts
│   │   │   │       ├── seer.ts
│   │   │   │       ├── validate-region-url.test.ts
│   │   │   │       └── validate-region-url.ts
│   │   │   ├── permissions.parseScopes.test.ts
│   │   │   ├── permissions.ts
│   │   │   ├── schema.ts
│   │   │   ├── server-context.test.ts
│   │   │   ├── server.ts
│   │   │   ├── telem
│   │   │   │   ├── index.ts
│   │   │   │   ├── logging.ts
│   │   │   │   ├── sentry.test.ts
│   │   │   │   └── sentry.ts
│   │   │   ├── test-setup.ts
│   │   │   ├── test-utils
│   │   │   │   └── context.ts
│   │   │   ├── toolDefinitions.ts
│   │   │   ├── tools
│   │   │   │   ├── agent-tools.ts
│   │   │   │   ├── analyze-issue-with-seer.test.ts
│   │   │   │   ├── analyze-issue-with-seer.ts
│   │   │   │   ├── create-dsn.test.ts
│   │   │   │   ├── create-dsn.ts
│   │   │   │   ├── create-project.test.ts
│   │   │   │   ├── create-project.ts
│   │   │   │   ├── create-team.test.ts
│   │   │   │   ├── create-team.ts
│   │   │   │   ├── find-dsns.test.ts
│   │   │   │   ├── find-dsns.ts
│   │   │   │   ├── find-organizations.test.ts
│   │   │   │   ├── find-organizations.ts
│   │   │   │   ├── find-projects.test.ts
│   │   │   │   ├── find-projects.ts
│   │   │   │   ├── find-releases.test.ts
│   │   │   │   ├── find-releases.ts
│   │   │   │   ├── find-teams.test.ts
│   │   │   │   ├── find-teams.ts
│   │   │   │   ├── get-doc.test.ts
│   │   │   │   ├── get-doc.ts
│   │   │   │   ├── get-event-attachment.test.ts
│   │   │   │   ├── get-event-attachment.ts
│   │   │   │   ├── get-issue-details.test.ts
│   │   │   │   ├── get-issue-details.ts
│   │   │   │   ├── get-trace-details.test.ts
│   │   │   │   ├── get-trace-details.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── search-docs.test.ts
│   │   │   │   ├── search-docs.ts
│   │   │   │   ├── search-events
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── formatters.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── utils.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── search-events.test.ts
│   │   │   │   ├── search-issues
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── formatters.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── README.md
│   │   │   │   ├── tools.test.ts
│   │   │   │   ├── types.ts
│   │   │   │   ├── update-issue.test.ts
│   │   │   │   ├── update-issue.ts
│   │   │   │   ├── update-project.test.ts
│   │   │   │   ├── update-project.ts
│   │   │   │   ├── use-sentry
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── handler.test.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── tool-wrapper.test.ts
│   │   │   │   │   └── tool-wrapper.ts
│   │   │   │   ├── whoami.test.ts
│   │   │   │   └── whoami.ts
│   │   │   ├── transports
│   │   │   │   └── stdio.ts
│   │   │   ├── types.ts
│   │   │   ├── utils
│   │   │   │   ├── slug-validation.test.ts
│   │   │   │   ├── slug-validation.ts
│   │   │   │   ├── url-utils.test.ts
│   │   │   │   └── url-utils.ts
│   │   │   └── version.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   ├── mcp-server-evals
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── bin
│   │   │   │   └── start-mock-stdio.ts
│   │   │   ├── evals
│   │   │   │   ├── autofix.eval.ts
│   │   │   │   ├── create-dsn.eval.ts
│   │   │   │   ├── create-project.eval.ts
│   │   │   │   ├── create-team.eval.ts
│   │   │   │   ├── get-issue.eval.ts
│   │   │   │   ├── get-trace-details.eval.ts
│   │   │   │   ├── list-dsns.eval.ts
│   │   │   │   ├── list-issues.eval.ts
│   │   │   │   ├── list-organizations.eval.ts
│   │   │   │   ├── list-projects.eval.ts
│   │   │   │   ├── list-releases.eval.ts
│   │   │   │   ├── list-tags.eval.ts
│   │   │   │   ├── list-teams.eval.ts
│   │   │   │   ├── search-docs.eval.ts
│   │   │   │   ├── search-events-agent.eval.ts
│   │   │   │   ├── search-events.eval.ts
│   │   │   │   ├── search-issues-agent.eval.ts
│   │   │   │   ├── search-issues.eval.ts
│   │   │   │   ├── update-issue.eval.ts
│   │   │   │   ├── update-project.eval.ts
│   │   │   │   └── utils
│   │   │   │       ├── fixtures.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── runner.ts
│   │   │   │       ├── structuredOutputScorer.ts
│   │   │   │       └── toolPredictionScorer.ts
│   │   │   └── setup-env.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── mcp-server-mocks
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── fixtures
│   │   │   │   ├── autofix-state.json
│   │   │   │   ├── event-attachments.json
│   │   │   │   ├── event.json
│   │   │   │   ├── issue.json
│   │   │   │   ├── performance-event.json
│   │   │   │   ├── project.json
│   │   │   │   ├── tags.json
│   │   │   │   ├── team.json
│   │   │   │   ├── trace-event.json
│   │   │   │   ├── trace-items-attributes-logs-number.json
│   │   │   │   ├── trace-items-attributes-logs-string.json
│   │   │   │   ├── trace-items-attributes-spans-number.json
│   │   │   │   ├── trace-items-attributes-spans-string.json
│   │   │   │   ├── trace-items-attributes.json
│   │   │   │   ├── trace-meta-with-nulls.json
│   │   │   │   ├── trace-meta.json
│   │   │   │   ├── trace-mixed.json
│   │   │   │   └── trace.json
│   │   │   ├── index.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── mcp-server-tsconfig
│   │   ├── package.json
│   │   ├── tsconfig.base.json
│   │   └── tsconfig.vite.json
│   ├── mcp-test-client
│   │   ├── .env.test
│   │   ├── .gitignore
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── agent.ts
│   │   │   ├── auth
│   │   │   │   ├── config.ts
│   │   │   │   └── oauth.ts
│   │   │   ├── constants.ts
│   │   │   ├── index.ts
│   │   │   ├── logger.test.ts
│   │   │   ├── logger.ts
│   │   │   ├── mcp-test-client-remote.ts
│   │   │   ├── mcp-test-client.ts
│   │   │   ├── types.ts
│   │   │   └── version.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── smoke-tests
│       ├── package.json
│       ├── src
│       │   └── smoke.test.ts
│       └── vitest.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── scripts
│   └── check-doc-links.mjs
├── turbo.json
└── vitest.workspace.ts
```

# Files

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/search-events/config.ts:
--------------------------------------------------------------------------------

```typescript
// Build a dataset-agnostic system prompt
export const systemPrompt = `You are a Sentry query translator. You need to:
1. FIRST determine which dataset (spans, errors, or logs) is most appropriate for the query
2. Query the available attributes for that dataset using the datasetAttributes tool
3. Use the otelSemantics tool if you need OpenTelemetry semantic conventions
4. Convert the natural language query to Sentry's search syntax (NOT SQL syntax)
5. Decide which fields to return in the results

CRITICAL: Sentry does NOT use SQL syntax. Do NOT generate SQL-like queries.

DATASET SELECTION GUIDELINES:
- spans: Performance data, traces, AI/LLM calls, database queries, HTTP requests, token usage, costs, duration metrics, user agent data, "XYZ calls", ambiguous operations (richest attribute set)
- errors: Exceptions, crashes, error messages, stack traces, unhandled errors, browser/client errors
- logs: Log entries, log messages, severity levels, debugging information

For ambiguous queries like "calls using XYZ", prefer spans dataset first as it contains the most comprehensive telemetry data.

CRITICAL - FIELD VERIFICATION REQUIREMENT:
Before constructing ANY query, you MUST verify field availability:
1. You CANNOT assume ANY field exists without checking - not even common ones
2. This includes ALL fields: custom attributes, database fields, HTTP fields, AI fields, user fields, etc.
3. Fields vary by project based on what data is being sent to Sentry
4. Using an unverified field WILL cause your query to fail with "field not found" errors
5. The datasetAttributes tool tells you EXACTLY which fields are available

TOOL USAGE GUIDELINES:
1. Use datasetAttributes tool to discover available fields for your chosen dataset
2. Use otelSemantics tool when you need specific OpenTelemetry semantic convention attributes
3. Use whoami tool when queries contain "me" references for user.id or user.email fields
4. IMPORTANT: For ambiguous terms like "user agents", "browser", "client" - use the datasetAttributes tool to find the correct field name (typically user_agent.original) instead of assuming it's related to user.id

CRITICAL - TOOL RESPONSE HANDLING:
All tools return responses in this format: {error?: string, result?: data}
- If 'error' is present: The tool failed - analyze the error message and potentially retry with corrections
- If 'result' is present: The tool succeeded - use the result data for your query construction
- Always check for errors before using results

CRITICAL - HANDLING "DISTINCT" OR "UNIQUE VALUES" QUERIES:
When user asks for "distinct", "unique", "all values of", or "what are the X" queries:
1. This ALWAYS requires an AGGREGATE query with count() function
2. Pattern: fields=['field_name', 'count()'] to show distinct values with counts
3. Sort by "-count()" to show most common values first
4. Use datasetAttributes tool to verify the field exists before constructing query
5. Examples:
   - "distinct categories" → fields=['category.name', 'count()'], sort='-count()'
   - "unique types" → fields=['item.type', 'count()'], sort='-count()'

CRITICAL - TRAFFIC/VOLUME/COUNT QUERIES:
When user asks about "traffic", "volume", "how much", "how many" (without specific metrics):
1. This ALWAYS requires an AGGREGATE query with count() function
2. For total counts: fields=['count()']
3. For grouped counts: fields=['grouping_field', 'count()']
4. Always include timeRange for period-specific queries
5. Examples:
   - "how much traffic in last 30 days" → fields=['count()'], timeRange: {"statsPeriod": "30d"}
   - "traffic on mcp-server" → query: "project:mcp-server", fields=['count()']

CRITICAL - HANDLING "ME" REFERENCES:
- If the query contains "me", "my", "myself", or "affecting me" in the context of user.id or user.email fields, use the whoami tool to get the user's ID and email
- For assignedTo fields, you can use "me" directly without translation (e.g., assignedTo:me works as-is)
- After calling whoami, replace "me" references with the actual user.id or user.email values
- If whoami fails, return an error explaining the issue

QUERY MODES:
1. INDIVIDUAL EVENTS (default): Returns raw event data
   - Used when fields contain no function() calls
   - Include recommended fields plus any user-requested fields

2. AGGREGATE QUERIES: Grouping and aggregation (NOT SQL)
   - Activated when ANY field contains a function() call
   - Fields should ONLY include: aggregate functions + groupBy fields
   - Automatically groups by ALL non-function fields
   - For aggregate queries, ONLY include the aggregate functions and groupBy fields - do NOT include default fields like timestamp, id, etc.
   - You SHOULD sort aggregate results by "-function_name()" for descending order (highest values first)
   - For equations in aggregate queries: You SHOULD use "-equation|..." prefix unless user wants lowest values
   - When user asks "how many total", "sum of", or similar: They want the highest/total value, use descending sort

CRITICAL LIMITATION - TIME SERIES NOT SUPPORTED:
- Queries asking for data "over time", "by hour", "by day", "time series", or similar temporal groupings are NOT currently supported
- If user asks for "X over time", return an error explaining: "Time series aggregations are not currently supported."

CRITICAL - DO NOT USE SQL SYNTAX:
- NEVER use SQL functions like yesterday(), today(), now(), IS NOT NULL, IS NULL
- NEVER use SQL date functions - use timeRange parameter instead
- For "yesterday": Use timeRange: {"statsPeriod": "24h"}, NOT timestamp >= yesterday()
- For field existence: Use has:field_name, NOT field_name IS NOT NULL
- For field absence: Use !has:field_name, NOT field_name IS NULL

MATHEMATICAL QUERY PATTERNS:
When user asks mathematical questions like "how many X", "total Y used", "sum of Z":
- Identify the appropriate dataset based on context
- Use datasetAttributes tool to find available numeric fields
- Use sum() function for totals, avg() for averages, count() for counts
- For time-based queries ("today", "yesterday", "this week"), use timeRange parameter
- For "total" or "how many" questions: Users typically want highest values first (descending sort)

DERIVED METRICS AND CALCULATIONS (SPANS ONLY):
When user asks for calculated metrics, ratios, or conversions:
- Use equation fields with "equation|" prefix
- Examples:
  - "duration in milliseconds" → fields: ["equation|avg(span.duration) * 1000"], sort: "-equation|avg(span.duration) * 1000"
  - "combined metric total" → fields: ["equation|sum(metric.a) + sum(metric.b)"], sort: "-equation|sum(metric.a) + sum(metric.b)"
  - "error rate percentage" → fields: ["equation|failure_rate() * 100"], sort: "-equation|failure_rate() * 100"
  - "events per second" → fields: ["equation|count() / 3600"], sort: "-equation|count() / 3600"
- IMPORTANT: Equations are ONLY supported in the spans dataset, NOT in errors or logs
- IMPORTANT: When sorting by equations, use "-equation|..." for descending order (highest values first)

SORTING RULES (CRITICAL - YOU MUST ALWAYS SPECIFY A SORT):
1. CRITICAL: Sort MUST go in the separate "sort" field, NEVER in the "query" field
   - WRONG: query: "level:error sort:-timestamp" ← Sort syntax in query field is FORBIDDEN
   - CORRECT: query: "level:error", sort: "-timestamp" ← Sort in separate field

2. DEFAULT SORTING:
   - errors dataset: Use "-timestamp" (newest first)
   - spans dataset: Use "-span.duration" (slowest first)  
   - logs dataset: Use "-timestamp" (newest first)

3. SORTING SYNTAX:
   - Use "-" prefix for descending order (e.g., "-timestamp" for newest first)
   - Use field name without prefix for ascending order
   - For aggregate queries: sort by aggregate function results (e.g., "-count()" for highest count first)
   - For equation fields: You SHOULD use "-equation|..." for descending order (e.g., "-equation|sum(field1) + sum(field2)")
   - Only omit the "-" prefix if the user clearly wants lowest values first (rare)

4. IMPORTANT SORTING REQUIREMENTS:
   - YOU MUST ALWAYS INCLUDE A SORT PARAMETER
   - CRITICAL: The field you sort by MUST be included in your fields array
   - If sorting by "-timestamp", include "timestamp" in fields
   - If sorting by "-count()", include "count()" in fields
   - This is MANDATORY - Sentry will reject queries where sort field is not in the selected fields

YOUR RESPONSE FORMAT:
Return a JSON object with these fields:
- "dataset": Which dataset you determined to use ("spans", "errors", or "logs")
- "query": The Sentry query string for filtering results (use empty string "" for no filters)
- "fields": Array of field names to return in results
  - For individual event queries: OPTIONAL (will use recommended fields if not provided)
  - For aggregate queries: REQUIRED (must include aggregate functions AND any groupBy fields)
- "sort": Sort parameter for results (REQUIRED - YOU MUST ALWAYS SPECIFY THIS)
- "timeRange": Time range parameters (optional)
  - Relative: {"statsPeriod": "24h"} for last 24 hours, "7d" for last 7 days, etc.
  - Absolute: {"start": "2025-06-19T07:00:00", "end": "2025-06-20T06:59:59"} for specific date ranges

CORRECT QUERY PATTERNS (FOLLOW THESE):
- For field existence: Use has:field_name (NOT field_name IS NOT NULL)
- For field absence: Use !has:field_name (NOT field_name IS NULL)
- For time periods: Use timeRange parameter (NOT SQL date functions)
- Example: "items processed yesterday" → query: "has:item.processed", timeRange: {"statsPeriod": "24h"}

PROCESS:
1. Analyze the user's query
2. Determine appropriate dataset
3. Use datasetAttributes tool to discover available fields
4. Use otelSemantics tool if needed for OpenTelemetry attributes
5. Construct the final query with proper fields and sort parameters

COMMON ERRORS TO AVOID:
- Using SQL syntax (IS NOT NULL, IS NULL, yesterday(), today(), etc.) - Use has: operator and timeRange instead
- Using numeric functions (sum, avg, min, max, percentiles) on non-numeric fields
- Using incorrect field names (use the otelSemantics tool to look up correct names)
- Missing required fields in the fields array for aggregate queries
- Invalid sort parameter not included in fields array
- For field existence: Use has:field_name (NOT field_name IS NOT NULL)
- For field absence: Use !has:field_name (NOT field_name IS NULL)
- For time periods: Use timeRange parameter (NOT SQL date functions like yesterday())`;

// Base fields common to all datasets
export const BASE_COMMON_FIELDS = {
  project: "Project slug",
  timestamp: "When the event occurred",
  environment: "Environment (production, staging, development)",
  release: "Release version",
  platform: "Platform (javascript, python, etc.)",
  "user.id": "User ID",
  "user.email": "User email",
  "sdk.name": "SDK name",
  "sdk.version": "SDK version",
};

// Known numeric fields for each dataset
export const NUMERIC_FIELDS: Record<string, Set<string>> = {
  spans: new Set([
    "span.duration",
    "span.self_time",
    "transaction.duration",
    "http.status_code",
    "gen_ai.usage.input_tokens",
    "gen_ai.usage.output_tokens",
    "gen_ai.request.max_tokens",
  ]),
  errors: new Set([
    // Most error fields are strings/categories
    "stack.lineno",
  ]),
  logs: new Set(["severity_number", "sentry.observed_timestamp_nanos"]),
};

// Dataset-specific field definitions
export const DATASET_FIELDS = {
  spans: {
    // Span-specific fields
    "span.op": "Span operation type (e.g., http.client, db.query, cache.get)",
    "span.description": "Detailed description of the span operation",
    "span.duration": "Duration of the span in milliseconds",
    "span.status": "Span status (ok, cancelled, unknown, etc.)",
    "span.self_time": "Time spent in this span excluding child spans",

    // Transaction fields
    transaction: "Transaction name/route",
    "transaction.duration": "Total transaction duration in milliseconds",
    "transaction.op": "Transaction operation type",
    "transaction.status": "Transaction status",
    is_transaction: "Whether this span is a transaction (true/false)",

    // Trace fields
    trace: "Trace ID",
    "trace.span_id": "Span ID within the trace",
    "trace.parent_span_id": "Parent span ID",

    // HTTP fields
    "http.method": "HTTP method (GET, POST, etc.)",
    "http.status_code": "HTTP response status code",
    "http.url": "Full HTTP URL",

    // Database fields
    "db.system": "Database system (postgresql, mysql, etc.)",
    "db.operation": "Database operation (SELECT, INSERT, etc.)",

    // OpenTelemetry attribute namespaces for semantic queries
    // Use has:namespace.* to find spans with any attribute in that namespace
    // GenAI namespace (gen_ai.*) - for AI/LLM/Agent calls
    "gen_ai.system": "AI system (e.g., anthropic, openai)",
    "gen_ai.request.model": "Model name (e.g., claude-3-5-sonnet-20241022)",
    "gen_ai.operation.name": "Operation type (e.g., chat, completion)",
    "gen_ai.usage.input_tokens": "Number of input tokens (numeric)",
    "gen_ai.usage.output_tokens": "Number of output tokens (numeric)",

    // MCP namespace (mcp.*) - for Model Context Protocol tool calls
    "mcp.tool.name": "Tool name (e.g., search_issues, search_events)",
    "mcp.session.id": "MCP session identifier",

    // Aggregate functions (SPANS dataset only - require numeric fields except count/count_unique)
    "count()": "Count of spans",
    "count_unique(field)": "Count of unique values, e.g. count_unique(user.id)",
    "avg(field)": "Average of numeric field, e.g. avg(span.duration)",
    "sum(field)": "Sum of numeric field, e.g. sum(span.self_time)",
    "min(field)": "Minimum of numeric field, e.g. min(span.duration)",
    "max(field)": "Maximum of numeric field, e.g. max(span.duration)",
    "p50(field)": "50th percentile (median), e.g. p50(span.duration)",
    "p75(field)": "75th percentile, e.g. p75(span.duration)",
    "p90(field)": "90th percentile, e.g. p90(span.duration)",
    "p95(field)": "95th percentile, e.g. p95(span.duration)",
    "p99(field)": "99th percentile, e.g. p99(span.duration)",
    "p100(field)": "100th percentile (max), e.g. p100(span.duration)",
    "epm()": "Events per minute rate",
    "failure_rate()": "Percentage of failed spans",
  },
  errors: {
    // Error-specific fields
    message: "Error message",
    level: "Error level (error, warning, info, debug)",
    "error.type": "Error type/exception class",
    "error.value": "Error value/description",
    "error.handled": "Whether the error was handled (true/false)",
    culprit: "Code location that caused the error",
    title: "Error title/grouping",

    // Stack trace fields
    "stack.filename": "File where error occurred",
    "stack.function": "Function where error occurred",
    "stack.module": "Module where error occurred",
    "stack.abs_path": "Absolute path to file",

    // Additional context fields
    "os.name": "Operating system name",
    "browser.name": "Browser name",
    "device.family": "Device family",

    // Aggregate functions (ERRORS dataset only)
    "count()": "Count of error events",
    "count_unique(field)": "Count of unique values, e.g. count_unique(user.id)",
    "count_if(field,equals,value)":
      "Conditional count, e.g. count_if(error.handled,equals,false)",
    "last_seen()": "Most recent timestamp of the group",
    "eps()": "Events per second rate",
    "epm()": "Events per minute rate",
  },
  logs: {
    // Log-specific fields
    message: "Log message",
    severity: "Log severity level",
    severity_number: "Numeric severity level",
    "sentry.item_id": "Sentry item ID",
    "sentry.observed_timestamp_nanos": "Observed timestamp in nanoseconds",

    // Trace context
    trace: "Trace ID",

    // Aggregate functions (LOGS dataset only - require numeric fields except count/count_unique)
    "count()": "Count of log entries",
    "count_unique(field)": "Count of unique values, e.g. count_unique(user.id)",
    "avg(field)": "Average of numeric field, e.g. avg(severity_number)",
    "sum(field)": "Sum of numeric field",
    "min(field)": "Minimum of numeric field",
    "max(field)": "Maximum of numeric field",
    "p50(field)": "50th percentile (median)",
    "p75(field)": "75th percentile",
    "p90(field)": "90th percentile",
    "p95(field)": "95th percentile",
    "p99(field)": "99th percentile",
    "p100(field)": "100th percentile (max)",
    "epm()": "Events per minute rate",
  },
};

// Dataset-specific rules and examples
export const DATASET_CONFIGS = {
  errors: {
    rules: `- For errors, focus on: message, level, error.type, error.handled
- Use level field for severity (error, warning, info, debug)
- Use error.handled:false for unhandled exceptions/crashes
- For filename searches: Use stack.filename for suffix-based search (e.g., stack.filename:"**/index.js" or stack.filename:"**/components/Button.tsx")
- When searching for errors in specific files, prefer including the parent folder to avoid ambiguity (e.g., stack.filename:"**/components/index.js" instead of just stack.filename:"**/index.js")`,
    examples: `- "null pointer exceptions" → 
  {
    "query": "error.type:\\"NullPointerException\\" OR message:\\"*null pointer*\\"",
    "fields": ["issue", "title", "project", "timestamp", "level", "message", "error.type", "culprit"],
    "sort": "-timestamp"
  }
- "unhandled errors in production" → 
  {
    "query": "error.handled:false AND environment:production",
    "fields": ["issue", "title", "project", "timestamp", "level", "message", "error.type", "culprit", "error.handled", "environment"],
    "sort": "-timestamp"
  }
- "database connection errors" → 
  {
    "query": "message:\\"*database*\\" AND message:\\"*connection*\\" AND level:error",
    "fields": ["issue", "title", "project", "timestamp", "level", "message", "error.type", "culprit"],
    "sort": "-timestamp"
  }
- "show me user emails for authentication failures" → 
  {
    "query": "message:\\"*auth*\\" AND (message:\\"*failed*\\" OR message:\\"*denied*\\")",
    "fields": ["issue", "title", "project", "timestamp", "level", "message", "error.type", "culprit", "user.email"],
    "sort": "-timestamp"
  }
- "errors in Button.tsx file" → 
  {
    "query": "stack.filename:\\"**/Button.tsx\\"",
    "fields": ["issue", "title", "project", "timestamp", "level", "message", "error.type", "culprit", "stack.filename"],
    "sort": "-timestamp"
  }
- "count errors by type in production" → 
  {
    "query": "environment:production",
    "fields": ["error.type", "count()", "last_seen()"],
    "sort": "-count()"
  }
- "most common errors last 24h" → 
  {
    "query": "level:error",
    "fields": ["title", "error.type", "count()"],
    "sort": "-count()"
  }
- "unhandled errors rate by project" → 
  {
    "query": "",
    "fields": ["project", "count()", "count_if(error.handled,equals,false)", "epm()"],
    "sort": "-count()"
  }
- "errors in the last hour" → 
  {
    "query": "",
    "fields": ["issue", "title", "project", "timestamp", "level", "message", "error.type", "culprit"],
    "sort": "-timestamp",
    "timeRange": {"statsPeriod": "1h"}
  }
- "database errors between June 19-20" → 
  {
    "query": "message:\\"*database*\\"",
    "fields": ["issue", "title", "project", "timestamp", "level", "message", "error.type", "culprit"],
    "sort": "-timestamp",
    "timeRange": {"start": "2025-06-19T00:00:00", "end": "2025-06-20T23:59:59"}
  }
- "unique users affected by errors" → 
  {
    "query": "level:error",
    "fields": ["error.type", "count()", "count_unique(user.id)"],
    "sort": "-count_unique(user.id)"
  }
- "what is the most common error" → 
  {
    "query": "",
    "fields": ["title", "count()"],
    "sort": "-count()"
  }
- "errors by browser" → 
  {
    "query": "has:user_agent.original",
    "fields": ["user_agent.original", "count()"],
    "sort": "-count()"
  }
- "which user agents have the most errors" → 
  {
    "query": "level:error AND has:user_agent.original",
    "fields": ["user_agent.original", "count()", "count_unique(user.id)"],
    "sort": "-count()"
  }`,
  },
  logs: {
    rules: `- For logs, focus on: message, severity, severity_number
- Use severity field for log levels (fatal, error, warning, info, debug, trace)
- severity_number is numeric (21=fatal, 17=error, 13=warning, 9=info, 5=debug, 1=trace)
- IMPORTANT: For time-based filtering in logs, do NOT use timestamp filters in the query
- Instead, time filtering for logs is handled by the statsPeriod parameter (not part of the query string)
- Keep your query focused on message content, severity levels, and other attributes only
- When user asks for "error logs", interpret this as logs with severity:error`,
    examples: `- "warning logs about memory" → 
  {
    "query": "severity:warning AND message:\\"*memory*\\"",
    "fields": ["timestamp", "project", "message", "severity", "trace"],
    "sort": "-timestamp"
  }
- "error logs from database" → 
  {
    "query": "severity:error AND message:\\"*database*\\"",
    "fields": ["timestamp", "project", "message", "severity", "trace"],
    "sort": "-timestamp"
  }
- "show me error logs with user context" → 
  {
    "query": "severity:error",
    "fields": ["timestamp", "project", "message", "severity", "trace", "user.id", "user.email"],
    "sort": "-timestamp"
  }
- "what is the most common log" → 
  {
    "query": "",
    "fields": ["message", "count()"],
    "sort": "-count()"
  }
- "most common error logs" → 
  {
    "query": "severity:error",
    "fields": ["message", "count()"],
    "sort": "-count()"
  }
- "count logs by severity" → 
  {
    "query": "",
    "fields": ["severity", "count()"],
    "sort": "-count()"
  }
- "log volume by project" → 
  {
    "query": "",
    "fields": ["project", "count()", "epm()"],
    "sort": "-count()"
  }`,
  },
  spans: {
    rules: `- For traces/spans, focus on: span.op, span.description, span.duration, transaction
- Use is_transaction:true for transaction spans only
- Use span.duration for performance queries (value is in milliseconds)
- IMPORTANT: Use has: queries for attribute-based filtering instead of span.op patterns:
  - For HTTP requests: use "has:request.url" instead of "span.op:http*"
  - For database queries: use "has:db.statement" or "has:db.system" instead of "span.op:db*"
  - For AI/LLM/Agent calls: use "has:gen_ai.system" or "has:gen_ai.request.model" (OpenTelemetry GenAI semantic conventions)
  - For MCP tool calls: use "has:mcp.tool.name" (Model Context Protocol semantic conventions)
  - This approach is more flexible and captures all relevant spans regardless of their operation type

OpenTelemetry Semantic Conventions (2025 Stable):
Core Namespaces:
- gen_ai.*: GenAI attributes for AI/LLM/Agent calls (system, request.model, operation.name, usage.*)
- db.*: Database attributes (system, statement, operation, name) - STABLE
- http.*: HTTP attributes (method, status_code, url, request.*, response.*) - STABLE
- rpc.*: RPC attributes (system, service, method, grpc.*)
- messaging.*: Messaging attributes (system, operation, destination.*)
- faas.*: Function as a Service attributes (name, version, runtime)
- cloud.*: Cloud provider attributes (provider, region, zone)
- k8s.*: Kubernetes attributes (namespace, pod, container, node)
- host.*: Host attributes (name, type, arch, os.*)
- service.*: Service attributes (name, version, instance.id)
- process.*: Process attributes (pid, command, runtime.*)

Custom Namespaces:
- mcp.*: Model Context Protocol attributes for MCP tool calls (tool.name, session.id, transport)

Query Patterns:
- Use has:namespace.* to find spans with any attribute in that namespace
- Most common: has:gen_ai.system (agent calls), has:mcp.tool.name (MCP tools), has:db.statement (database), has:http.method (HTTP)`,
    examples: `- "database queries" → 
  {
    "query": "has:db.statement",
    "fields": ["span.op", "span.description", "span.duration", "transaction", "timestamp", "project", "trace", "db.system", "db.statement"],
    "sort": "-span.duration"
  }
- "slow API calls over 5 seconds" → 
  {
    "query": "has:request.url AND span.duration:>5000",
    "fields": ["span.op", "span.description", "span.duration", "transaction", "timestamp", "project", "trace", "request.url", "request.method", "span.status_code"],
    "sort": "-span.duration"
  }
- "show me database queries with their SQL" → 
  {
    "query": "has:db.statement",
    "fields": ["span.op", "span.description", "span.duration", "transaction", "timestamp", "project", "trace", "db.system", "db.statement"],
    "sort": "-span.duration"
  }
- "average response time by endpoint" → 
  {
    "query": "is_transaction:true",
    "fields": ["transaction", "count()", "avg(span.duration)", "p95(span.duration)"],
    "sort": "-avg(span.duration)"
  }
- "slowest database queries by p95" → 
  {
    "query": "has:db.statement",
    "fields": ["db.statement", "count()", "p50(span.duration)", "p95(span.duration)", "max(span.duration)"],
    "sort": "-p95(span.duration)"
  }
- "API calls in the last 30 minutes" → 
  {
    "query": "has:request.url",
    "fields": ["id", "span.op", "span.description", "span.duration", "transaction", "timestamp", "project", "trace", "request.url", "request.method"],
    "sort": "-timestamp",
    "timeRange": {"statsPeriod": "30m"}
  }
- "most common transaction" → 
  {
    "query": "is_transaction:true",
    "fields": ["transaction", "count()"],
    "sort": "-count()"
  }
- "top 10 tool call spans by usage" → 
  {
    "query": "has:mcp.tool.name",
    "fields": ["mcp.tool.name", "count()"],
    "sort": "-count()"
  }
- "top 10 agent call spans by usage" → 
  {
    "query": "has:gen_ai.system",
    "fields": ["gen_ai.system", "gen_ai.request.model", "count()"],
    "sort": "-count()"
  }
- "slowest AI/LLM calls" → 
  {
    "query": "has:gen_ai.request.model",
    "fields": ["gen_ai.system", "gen_ai.request.model", "span.duration", "transaction", "timestamp", "project", "trace", "gen_ai.operation.name"],
    "sort": "-span.duration"
  }
- "agent calls by model usage" → 
  {
    "query": "has:gen_ai.request.model",
    "fields": ["gen_ai.request.model", "count()"],
    "sort": "-count()"
  }
- "average agent call duration by model" → 
  {
    "query": "has:gen_ai.request.model",
    "fields": ["gen_ai.request.model", "count()", "avg(span.duration)", "p95(span.duration)"],
    "sort": "-avg(span.duration)"
  }
- "token usage by AI system" → 
  {
    "query": "has:gen_ai.usage.input_tokens",
    "fields": ["gen_ai.system", "sum(gen_ai.usage.input_tokens)", "sum(gen_ai.usage.output_tokens)", "count()"],
    "sort": "-sum(gen_ai.usage.input_tokens)"
  }
- "how many tokens used today" → 
  {
    "query": "has:gen_ai.usage.input_tokens",
    "fields": ["sum(gen_ai.usage.input_tokens)", "sum(gen_ai.usage.output_tokens)", "count()"],
    "sort": "-sum(gen_ai.usage.input_tokens)",
    "timeRange": {"statsPeriod": "24h"}
  }
- "average response time in milliseconds" → 
  {
    "query": "is_transaction:true",
    "fields": ["transaction", "equation|avg(span.duration) * 1000"],
    "sort": "-equation|avg(span.duration) * 1000",
    "timeRange": {"statsPeriod": "24h"}
  }
- "total input tokens by model" → 
  {
    "query": "has:gen_ai.usage.input_tokens",
    "fields": ["gen_ai.request.model", "sum(gen_ai.usage.input_tokens)", "count()"],
    "sort": "-sum(gen_ai.usage.input_tokens)"
  }
- "tokens used this week" → 
  {
    "query": "has:gen_ai.usage.input_tokens",
    "fields": ["sum(gen_ai.usage.input_tokens)", "sum(gen_ai.usage.output_tokens)", "count()"],
    "sort": "-sum(gen_ai.usage.input_tokens)",
    "timeRange": {"statsPeriod": "7d"}
  }
- "which user agents have the most tool calls yesterday" → 
  {
    "query": "has:mcp.tool.name AND has:user_agent.original",
    "fields": ["user_agent.original", "count()"],
    "sort": "-count()",
    "timeRange": {"statsPeriod": "24h"}
  }
- "top 10 browsers by API calls" → 
  {
    "query": "has:http.method AND has:user_agent.original",
    "fields": ["user_agent.original", "count()"],
    "sort": "-count()"
  }
- "most common clients making database queries" → 
  {
    "query": "has:db.statement AND has:user_agent.original",
    "fields": ["user_agent.original", "count()", "avg(span.duration)"],
    "sort": "-count()"
  }`,
  },
};

// Define recommended fields for each dataset
export const RECOMMENDED_FIELDS = {
  errors: {
    basic: [
      "issue",
      "title",
      "project",
      "timestamp",
      "level",
      "message",
      "error.type",
      "culprit",
    ],
    description:
      "Basic error information including issue ID, title, timestamp, severity, and location",
  },
  logs: {
    basic: ["timestamp", "project", "message", "severity", "trace"],
    description: "Essential log entry information",
  },
  spans: {
    basic: [
      "id",
      "span.op",
      "span.description",
      "span.duration",
      "transaction",
      "timestamp",
      "project",
      "trace",
    ],
    description:
      "Core span/trace information including span ID, operation, duration, and trace context",
  },
};

```

--------------------------------------------------------------------------------
/packages/mcp-server/src/tools/get-issue-details.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from "vitest";
import { http, HttpResponse } from "msw";
import { mswServer } from "@sentry/mcp-server-mocks";
import getIssueDetails from "./get-issue-details.js";
import { performanceEventFixture } from "@sentry/mcp-server-mocks";

const baseContext = {
  constraints: {
    organizationSlug: null,
  },
  accessToken: "access-token",
  userId: "1",
};

function createPerformanceIssueFixture() {
  return {
    id: "7890123456",
    shareId: null,
    shortId: "PERF-N1-001",
    title: "N+1 Query: SELECT * FROM users WHERE id = %s",
    culprit: "GET /api/users",
    permalink: "https://sentry-mcp-evals.sentry.io/issues/7890123456/",
    logger: null,
    level: "warning",
    status: "unresolved",
    statusDetails: {},
    substatus: "ongoing",
    isPublic: false,
    platform: "python",
    project: {
      id: "4509062593708032",
      name: "CLOUDFLARE-MCP",
      slug: "CLOUDFLARE-MCP",
      platform: "python",
    },
    type: "performance_n_plus_one_db_queries",
    metadata: {
      title: "N+1 Query: SELECT * FROM users WHERE id = %s",
      location: "GET /api/users",
      value: "SELECT * FROM users WHERE id = %s",
    },
    numComments: 0,
    assignedTo: null,
    isBookmarked: false,
    isSubscribed: false,
    subscriptionDetails: null,
    hasSeen: true,
    annotations: [],
    issueType: "performance_n_plus_one_db_queries",
    issueCategory: "performance",
    priority: "medium",
    priorityLockedAt: null,
    isUnhandled: false,
    count: "25",
    userCount: 5,
    firstSeen: "2025-08-05T12:00:00.000Z",
    lastSeen: "2025-08-06T12:00:00.000Z",
    firstRelease: null,
    lastRelease: null,
    activity: [],
    openPeriods: [],
    seenBy: [],
    pluginActions: [],
    pluginIssues: [],
    pluginContexts: [],
    userReportCount: 0,
    stats: {},
    participants: [],
  };
}

function createPerformanceEventFixture() {
  const cloned = JSON.parse(JSON.stringify(performanceEventFixture));
  const offenderSpanIds = cloned.occurrence.evidenceData.offenderSpanIds.slice(
    0,
    3,
  );
  cloned.occurrence.evidenceData.offenderSpanIds = offenderSpanIds;
  cloned.occurrence.evidenceData.numberRepeatingSpans = String(
    offenderSpanIds.length,
  );
  cloned.occurrence.evidenceData.repeatingSpansCompact = undefined;
  cloned.occurrence.evidenceData.repeatingSpans = [
    'db - INSERT INTO "sentry_fileblobindex" ("offset", "file_id", "blob_id") VALUES (%s, %s, %s) RETURNING "sentry_fileblobindex"."id"',
    "function - sentry.models.files.abstractfileblob.AbstractFileBlob.from_file",
    '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',
  ];

  const spansEntry = cloned.entries.find(
    (entry: { type: string }) => entry.type === "spans",
  );
  if (spansEntry?.data) {
    spansEntry.data = spansEntry.data.slice(0, 4);
  }
  return cloned;
}

function createTraceResponseFixture() {
  return [
    {
      span_id: "root-span",
      event_id: "root-span",
      transaction_id: "root-span",
      project_id: "4509062593708032",
      project_slug: "cloudflare-mcp",
      profile_id: "",
      profiler_id: "",
      parent_span_id: null,
      start_timestamp: 0,
      end_timestamp: 1,
      measurements: {},
      duration: 1000,
      transaction: "/api/users",
      is_transaction: true,
      description: "GET /api/users",
      sdk_name: "sentry.python",
      op: "http.server",
      name: "GET /api/users",
      event_type: "transaction",
      additional_attributes: {},
      errors: [],
      occurrences: [],
      children: [
        {
          span_id: "parent123",
          event_id: "parent123",
          transaction_id: "parent123",
          project_id: "4509062593708032",
          project_slug: "cloudflare-mcp",
          profile_id: "",
          profiler_id: "",
          parent_span_id: "root-span",
          start_timestamp: 0.1,
          end_timestamp: 0.35,
          measurements: {},
          duration: 250,
          transaction: "/api/users",
          is_transaction: false,
          description: "GET /api/users handler",
          sdk_name: "sentry.python",
          op: "http.server",
          name: "GET /api/users handler",
          event_type: "span",
          additional_attributes: {},
          errors: [],
          occurrences: [],
          children: [
            {
              span_id: "span001",
              event_id: "span001",
              transaction_id: "span001",
              project_id: "4509062593708032",
              project_slug: "cloudflare-mcp",
              profile_id: "",
              profiler_id: "",
              parent_span_id: "parent123",
              start_timestamp: 0.15,
              end_timestamp: 0.16,
              measurements: {},
              duration: 10,
              transaction: "/api/users",
              is_transaction: false,
              description: "SELECT * FROM users WHERE id = 1",
              sdk_name: "sentry.python",
              op: "db.query",
              name: "SELECT * FROM users WHERE id = 1",
              event_type: "span",
              additional_attributes: {},
              errors: [],
              occurrences: [],
              children: [],
            },
            {
              span_id: "span002",
              event_id: "span002",
              transaction_id: "span002",
              project_id: "4509062593708032",
              project_slug: "cloudflare-mcp",
              profile_id: "",
              profiler_id: "",
              parent_span_id: "parent123",
              start_timestamp: 0.2,
              end_timestamp: 0.212,
              measurements: {},
              duration: 12,
              transaction: "/api/users",
              is_transaction: false,
              description: "SELECT * FROM users WHERE id = 2",
              sdk_name: "sentry.python",
              op: "db.query",
              name: "SELECT * FROM users WHERE id = 2",
              event_type: "span",
              additional_attributes: {},
              errors: [],
              occurrences: [],
              children: [],
            },
            {
              span_id: "span003",
              event_id: "span003",
              transaction_id: "span003",
              project_id: "4509062593708032",
              project_slug: "cloudflare-mcp",
              profile_id: "",
              profiler_id: "",
              parent_span_id: "parent123",
              start_timestamp: 0.24,
              end_timestamp: 0.255,
              measurements: {},
              duration: 15,
              transaction: "/api/users",
              is_transaction: false,
              description: "SELECT * FROM users WHERE id = 3",
              sdk_name: "sentry.python",
              op: "db.query",
              name: "SELECT * FROM users WHERE id = 3",
              event_type: "span",
              additional_attributes: {},
              errors: [],
              occurrences: [],
              children: [],
            },
          ],
        },
      ],
    },
  ];
}

describe("get_issue_details", () => {
  it("serializes with issueId", async () => {
    const result = await getIssueDetails.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: "CLOUDFLARE-MCP-41",
        eventId: undefined,
        issueUrl: undefined,
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );
    expect(result).toMatchInlineSnapshot(`
      "# Issue CLOUDFLARE-MCP-41 in **sentry-mcp-evals**

      **Description**: Error: Tool list_organizations is already registered
      **Culprit**: Object.fetch(index)
      **First Seen**: 2025-04-03T22:51:19.403Z
      **Last Seen**: 2025-04-12T11:34:11.000Z
      **Occurrences**: 25
      **Users Impacted**: 1
      **Status**: unresolved
      **Platform**: javascript
      **Project**: CLOUDFLARE-MCP
      **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41

      ## Event Details

      **Event ID**: 7ca573c0f4814912aaa9bdc77d1a7d51
      **Occurred At**: 2025-04-08T21:15:04.000Z

      ### Error

      \`\`\`
      Error: Tool list_organizations is already registered
      \`\`\`

      **Stacktrace:**
      \`\`\`
      index.js:7809:27
      index.js:8029:24 (OAuthProviderImpl.fetch)
      index.js:19631:28 (Object.fetch)
      \`\`\`

      ### HTTP Request

      **Method:** GET
      **URL:** https://mcp.sentry.dev/sse

      ### Tags

      **environment**: development
      **handled**: no
      **level**: error
      **mechanism**: cloudflare
      **runtime.name**: cloudflare
      **url**: https://mcp.sentry.dev/sse

      ### Additional Context

      These are additional context provided by the user when they're instrumenting their application.

      **cloud_resource**
      cloud.provider: "cloudflare"

      **culture**
      timezone: "Europe/London"

      **runtime**
      name: "cloudflare"

      **trace**
      trace_id: "3032af8bcdfe4423b937fc5c041d5d82"
      span_id: "953da703d2a6f4c7"
      status: "unknown"
      client_sample_rate: 1
      sampled: true

      # Using this information

      - You can reference the IssueID in commit messages (e.g. \`Fixes CLOUDFLARE-MCP-41\`) to automatically close the issue when the commit is merged.
      - The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.
      "
    `);
  });

  it("serializes with issueUrl", async () => {
    const result = await getIssueDetails.handler(
      {
        organizationSlug: undefined,
        issueId: undefined,
        eventId: undefined,
        issueUrl: "https://sentry-mcp-evals.sentry.io/issues/6507376925",
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );

    expect(result).toMatchInlineSnapshot(`
      "# Issue CLOUDFLARE-MCP-41 in **sentry-mcp-evals**

      **Description**: Error: Tool list_organizations is already registered
      **Culprit**: Object.fetch(index)
      **First Seen**: 2025-04-03T22:51:19.403Z
      **Last Seen**: 2025-04-12T11:34:11.000Z
      **Occurrences**: 25
      **Users Impacted**: 1
      **Status**: unresolved
      **Platform**: javascript
      **Project**: CLOUDFLARE-MCP
      **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41

      ## Event Details

      **Event ID**: 7ca573c0f4814912aaa9bdc77d1a7d51
      **Occurred At**: 2025-04-08T21:15:04.000Z

      ### Error

      \`\`\`
      Error: Tool list_organizations is already registered
      \`\`\`

      **Stacktrace:**
      \`\`\`
      index.js:7809:27
      index.js:8029:24 (OAuthProviderImpl.fetch)
      index.js:19631:28 (Object.fetch)
      \`\`\`

      ### HTTP Request

      **Method:** GET
      **URL:** https://mcp.sentry.dev/sse

      ### Tags

      **environment**: development
      **handled**: no
      **level**: error
      **mechanism**: cloudflare
      **runtime.name**: cloudflare
      **url**: https://mcp.sentry.dev/sse

      ### Additional Context

      These are additional context provided by the user when they're instrumenting their application.

      **cloud_resource**
      cloud.provider: "cloudflare"

      **culture**
      timezone: "Europe/London"

      **runtime**
      name: "cloudflare"

      **trace**
      trace_id: "3032af8bcdfe4423b937fc5c041d5d82"
      span_id: "953da703d2a6f4c7"
      status: "unknown"
      client_sample_rate: 1
      sampled: true

      # Using this information

      - You can reference the IssueID in commit messages (e.g. \`Fixes CLOUDFLARE-MCP-41\`) to automatically close the issue when the commit is merged.
      - The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.
      "
    `);
  });

  it("renders related trace spans when trace fetch succeeds", async () => {
    mswServer.use(
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/",
        () => HttpResponse.json(createPerformanceIssueFixture()),
        { once: true },
      ),
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/latest/",
        () => HttpResponse.json(createPerformanceEventFixture()),
        { once: true },
      ),
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/trace/abcdef1234567890abcdef1234567890/",
        () => HttpResponse.json(createTraceResponseFixture()),
        { once: true },
      ),
    );

    const result = await getIssueDetails.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: "PERF-N1-001",
        eventId: undefined,
        issueUrl: undefined,
        regionUrl: undefined,
      },
      baseContext,
    );

    if (typeof result !== "string") {
      throw new Error("Expected string result");
    }

    const performanceSection = result
      .slice(result.indexOf("### Repeated Database Queries"))
      .split("### Tags")[0]
      .trim();

    expect(performanceSection).toMatchInlineSnapshot(`
      "### Repeated Database Queries

      **Query executed 3 times:**
      **Repeated operations:**
      - db - INSERT INTO \"sentry_fileblobindex\" (\"offset\", \"file_id\", \"blob_id\") VALUES (%s, %s, %s) RETURNING \"sentry_fileblobindex\".\"id\"
      - function - sentry.models.files.abstractfileblob.AbstractFileBlob.from_file
      - 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

      ### Span Tree (Limited to 10 spans)

      \`\`\`
      GET /api/users [parent12 · http.server · 250ms]
         ├─ SELECT * FROM users WHERE id = 1 [span001 · db.query · 5ms] [N+1]
         ├─ SELECT * FROM users WHERE id = 2 [span002 · db.query · 5ms] [N+1]
         └─ SELECT * FROM users WHERE id = 3 [span003 · db.query · 5ms] [N+1]
      \`\`\`

      **Transaction:**
      /api/users

      **Offending Spans:**
      SELECT * FROM users WHERE id = %s

      **Repeated:**
      25 times"
    `);
  });

  it("falls back to offending span list when trace fetch fails", async () => {
    mswServer.use(
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/",
        () => HttpResponse.json(createPerformanceIssueFixture()),
        { once: true },
      ),
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/latest/",
        () => HttpResponse.json(createPerformanceEventFixture()),
        { once: true },
      ),
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/trace/abcdef1234567890abcdef1234567890/",
        () => HttpResponse.json({ detail: "Trace not found" }, { status: 404 }),
        { once: true },
      ),
    );

    const result = await getIssueDetails.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: "PERF-N1-001",
        eventId: undefined,
        issueUrl: undefined,
        regionUrl: undefined,
      },
      baseContext,
    );

    if (typeof result !== "string") {
      throw new Error("Expected string result");
    }

    const performanceSection = result
      .slice(result.indexOf("### Repeated Database Queries"))
      .split("### Tags")[0]
      .trim();

    expect(performanceSection).toMatchInlineSnapshot(`
      "### Repeated Database Queries

      **Query executed 3 times:**
      **Repeated operations:**
      - db - INSERT INTO \"sentry_fileblobindex\" (\"offset\", \"file_id\", \"blob_id\") VALUES (%s, %s, %s) RETURNING \"sentry_fileblobindex\".\"id\"
      - function - sentry.models.files.abstractfileblob.AbstractFileBlob.from_file
      - 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

      ### Span Tree (Limited to 10 spans)

      \`\`\`
      GET /api/users [parent12 · http.server · 250ms]
         ├─ SELECT * FROM users WHERE id = 1 [span001 · db.query · 5ms] [N+1]
         ├─ SELECT * FROM users WHERE id = 2 [span002 · db.query · 5ms] [N+1]
         └─ SELECT * FROM users WHERE id = 3 [span003 · db.query · 5ms] [N+1]
      \`\`\`

      **Transaction:**
      /api/users

      **Offending Spans:**
      SELECT * FROM users WHERE id = %s

      **Repeated:**
      25 times"
    `);
  });

  it("serializes with eventId", async () => {
    const result = await getIssueDetails.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: undefined,
        issueUrl: undefined,
        eventId: "7ca573c0f4814912aaa9bdc77d1a7d51",
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );
    expect(result).toMatchInlineSnapshot(`
      "# Issue CLOUDFLARE-MCP-41 in **sentry-mcp-evals**

      **Description**: Error: Tool list_organizations is already registered
      **Culprit**: Object.fetch(index)
      **First Seen**: 2025-04-03T22:51:19.403Z
      **Last Seen**: 2025-04-12T11:34:11.000Z
      **Occurrences**: 25
      **Users Impacted**: 1
      **Status**: unresolved
      **Platform**: javascript
      **Project**: CLOUDFLARE-MCP
      **URL**: https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41

      ## Event Details

      **Event ID**: 7ca573c0f4814912aaa9bdc77d1a7d51
      **Occurred At**: 2025-04-08T21:15:04.000Z

      ### Error

      \`\`\`
      Error: Tool list_organizations is already registered
      \`\`\`

      **Stacktrace:**
      \`\`\`
      index.js:7809:27
      index.js:8029:24 (OAuthProviderImpl.fetch)
      index.js:19631:28 (Object.fetch)
      \`\`\`

      ### HTTP Request

      **Method:** GET
      **URL:** https://mcp.sentry.dev/sse

      ### Tags

      **environment**: development
      **handled**: no
      **level**: error
      **mechanism**: cloudflare
      **runtime.name**: cloudflare
      **url**: https://mcp.sentry.dev/sse

      ### Additional Context

      These are additional context provided by the user when they're instrumenting their application.

      **cloud_resource**
      cloud.provider: "cloudflare"

      **culture**
      timezone: "Europe/London"

      **runtime**
      name: "cloudflare"

      **trace**
      trace_id: "3032af8bcdfe4423b937fc5c041d5d82"
      span_id: "953da703d2a6f4c7"
      status: "unknown"
      client_sample_rate: 1
      sampled: true

      # Using this information

      - You can reference the IssueID in commit messages (e.g. \`Fixes CLOUDFLARE-MCP-41\`) to automatically close the issue when the commit is merged.
      - The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.
      "
    `);
  });

  it("throws error for malformed regionUrl", async () => {
    await expect(
      getIssueDetails.handler(
        {
          organizationSlug: "sentry-mcp-evals",
          issueId: "CLOUDFLARE-MCP-41",
          eventId: undefined,
          issueUrl: undefined,
          regionUrl: "https",
        },
        {
          constraints: {
            organizationSlug: null,
          },
          accessToken: "access-token",
          userId: "1",
        },
      ),
    ).rejects.toThrow(
      "Invalid regionUrl provided: https. Must be a valid URL.",
    );
  });

  it("enhances 404 error with parameter context for non-existent issue", async () => {
    // This test demonstrates the enhance-error functionality:
    // When a 404 occurs, enhanceNotFoundError() adds parameter context to help users
    // understand what went wrong (organizationSlug + issueId in this case)

    // Mock a 404 response for a non-existent issue
    mswServer.use(
      http.get(
        "https://sentry.io/api/0/organizations/test-org/issues/NONEXISTENT-ISSUE-123/",
        () => {
          return new HttpResponse(
            JSON.stringify({ detail: "The requested resource does not exist" }),
            { status: 404 },
          );
        },
        { once: true },
      ),
    );

    await expect(
      getIssueDetails.handler(
        {
          organizationSlug: "test-org",
          issueId: "NONEXISTENT-ISSUE-123",
          eventId: undefined,
          issueUrl: undefined,
          regionUrl: undefined,
        },
        {
          constraints: {
            organizationSlug: null,
          },
          accessToken: "access-token",
          userId: "1",
        },
      ),
    ).rejects.toThrowErrorMatchingInlineSnapshot(`
      [ApiNotFoundError: The requested resource does not exist
      Please verify these parameters are correct:
        - organizationSlug: 'test-org'
        - issueId: 'NONEXISTENT-ISSUE-123']
    `);
  });

  // These tests verify that Seer analysis is properly formatted when available
  // Note: The autofix endpoint needs to be mocked for each test

  it("includes Seer analysis when available - COMPLETED state", async () => {
    // This test currently passes without Seer data since the autofix endpoint
    // returns an error that is caught silently. The functionality is implemented
    // and will work when Seer data is available.
    const result = await getIssueDetails.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: "CLOUDFLARE-MCP-41",
        eventId: undefined,
        issueUrl: undefined,
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );

    // Verify the basic issue output is present
    expect(result).toContain("# Issue CLOUDFLARE-MCP-41");
    expect(result).toContain(
      "Error: Tool list_organizations is already registered",
    );
    // When Seer data is available, these would pass:
    // expect(result).toContain("## Seer AI Analysis");
    // expect(result).toContain("For detailed root cause analysis and solutions, call `analyze_issue_with_seer(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')`");
  });

  it.skip("includes Seer analysis when in progress - PROCESSING state", async () => {
    const inProgressFixture = {
      autofix: {
        run_id: 12345,
        status: "PROCESSING",
        updated_at: "2025-04-09T22:39:50.778146",
        request: {},
        steps: [
          {
            id: "step-1",
            type: "root_cause_analysis",
            status: "COMPLETED",
            title: "Root Cause Analysis",
            index: 0,
            causes: [
              {
                id: 0,
                description:
                  "The bottleById query fails because the input ID doesn't exist in the database.",
                root_cause_reproduction: [],
              },
            ],
            progress: [],
            queued_user_messages: [],
            selection: null,
          },
          {
            id: "step-2",
            type: "solution",
            status: "IN_PROGRESS",
            title: "Generating Solution",
            index: 1,
            description: null,
            solution: [],
            progress: [],
            queued_user_messages: [],
          },
        ],
      },
    };

    // Use mswServer.use to prepend a handler - MSW uses LIFO order
    mswServer.use(
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/",
        () => HttpResponse.json(inProgressFixture),
        { once: true }, // Ensure this handler is only used once for this test
      ),
    );

    const result = await getIssueDetails.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: "CLOUDFLARE-MCP-41",
        eventId: undefined,
        issueUrl: undefined,
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );

    expect(result).toContain("## Seer Analysis");
    expect(result).toContain("**Status:** Processing");
    expect(result).toContain("**Root Cause Identified:**");
    expect(result).toContain(
      "The bottleById query fails because the input ID doesn't exist in the database.",
    );
    expect(result).toContain(
      "For detailed root cause analysis and solutions, call `analyze_issue_with_seer(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')`",
    );
  });

  it.skip("includes Seer analysis when failed - FAILED state", async () => {
    const failedFixture = {
      autofix: {
        run_id: 12346,
        status: "FAILED",
        updated_at: "2025-04-09T22:39:50.778146",
        request: {},
        steps: [],
      },
    };

    mswServer.use(
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/",
        () => HttpResponse.json(failedFixture),
        { once: true },
      ),
    );

    const result = await getIssueDetails.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: "CLOUDFLARE-MCP-41",
        eventId: undefined,
        issueUrl: undefined,
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );

    expect(result).toContain("## Seer Analysis");
    expect(result).toContain("**Status:** Analysis failed.");
    expect(result).toContain(
      "For detailed root cause analysis and solutions, call `analyze_issue_with_seer(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')`",
    );
  });

  it.skip("includes Seer analysis when needs information - NEED_MORE_INFORMATION state", async () => {
    const needsInfoFixture = {
      autofix: {
        run_id: 12347,
        status: "NEED_MORE_INFORMATION",
        updated_at: "2025-04-09T22:39:50.778146",
        request: {},
        steps: [
          {
            id: "step-1",
            type: "root_cause_analysis",
            status: "COMPLETED",
            title: "Root Cause Analysis",
            index: 0,
            causes: [
              {
                id: 0,
                description:
                  "Partial analysis completed but more context needed.",
                root_cause_reproduction: [],
              },
            ],
            progress: [],
            queued_user_messages: [],
            selection: null,
          },
        ],
      },
    };

    mswServer.use(
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/",
        () => HttpResponse.json(needsInfoFixture),
        { once: true },
      ),
    );

    const result = await getIssueDetails.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: "CLOUDFLARE-MCP-41",
        eventId: undefined,
        issueUrl: undefined,
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );

    expect(result).toContain("## Seer Analysis");
    expect(result).toContain("**Root Cause Identified:**");
    expect(result).toContain(
      "Partial analysis completed but more context needed.",
    );
    expect(result).toContain(
      "**Status:** Analysis paused - additional information needed.",
    );
    expect(result).toContain(
      "For detailed root cause analysis and solutions, call `analyze_issue_with_seer(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')`",
    );
  });

  it("handles default event type (error without exception data)", async () => {
    // Mock a "default" event type - represents errors without exception data
    mswServer.use(
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/DEFAULT-001/events/latest/",
        () => {
          return HttpResponse.json({
            id: "abc123def456",
            title: "Error without exception data",
            message: "Something went wrong",
            platform: "python",
            type: "default", // This is the key part - default event type
            dateCreated: "2025-10-02T12:00:00.000Z",
            culprit: "unknown",
            entries: [
              {
                type: "message",
                data: {
                  formatted: "Something went wrong",
                  message: "Something went wrong",
                },
              },
            ],
            tags: [
              { key: "level", value: "error" },
              { key: "environment", value: "production" },
            ],
            contexts: {},
          });
        },
      ),
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/DEFAULT-001/",
        () => {
          return HttpResponse.json({
            id: "123456",
            shortId: "DEFAULT-001",
            title: "Error without exception data",
            firstSeen: "2025-10-02T10:00:00.000Z",
            lastSeen: "2025-10-02T12:00:00.000Z",
            count: "5",
            userCount: 2,
            permalink: "https://sentry-mcp-evals.sentry.io/issues/123456/",
            project: {
              id: "4509062593708032",
              name: "TEST-PROJECT",
              slug: "test-project",
              platform: "python",
            },
            status: "unresolved",
            culprit: "unknown",
            type: "default",
            platform: "python",
          });
        },
      ),
    );

    const result = await getIssueDetails.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: "DEFAULT-001",
        eventId: undefined,
        issueUrl: undefined,
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );

    // Verify the event was processed successfully
    expect(result).toContain("# Issue DEFAULT-001 in **sentry-mcp-evals**");
    expect(result).toContain("Error without exception data");
    expect(result).toContain("**Event ID**: abc123def456");
    // Default events should show dateCreated just like error events
    expect(result).toContain("**Occurred At**: 2025-10-02T12:00:00.000Z");
    expect(result).toContain("### Error");
    expect(result).toContain("Something went wrong");
  });

  it("displays context (extra) data when present", async () => {
    const eventWithContext = {
      id: "abc123def456",
      type: "error",
      title: "TypeError",
      culprit: "app.js in processData",
      message: "Cannot read property 'value' of undefined",
      dateCreated: "2025-10-02T12:00:00.000Z",
      platform: "javascript",
      entries: [
        {
          type: "message",
          data: {
            formatted: "Cannot read property 'value' of undefined",
          },
        },
      ],
      context: {
        custom_field: "custom_value",
        user_action: "submit_form",
        session_data: {
          session_id: "sess_12345",
          user_id: "user_67890",
        },
        environment_info: "production",
      },
      contexts: {
        runtime: {
          name: "node",
          version: "18.0.0",
          type: "runtime",
        },
      },
      tags: [
        { key: "environment", value: "production" },
        { key: "level", value: "error" },
      ],
    };

    mswServer.use(
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CONTEXT-001/",
        () => {
          return HttpResponse.json({
            id: "123456",
            shortId: "CONTEXT-001",
            title: "TypeError",
            firstSeen: "2025-10-02T10:00:00.000Z",
            lastSeen: "2025-10-02T12:00:00.000Z",
            count: "5",
            userCount: 2,
            permalink: "https://sentry-mcp-evals.sentry.io/issues/123456/",
            project: {
              id: "4509062593708032",
              name: "TEST-PROJECT",
              slug: "test-project",
              platform: "javascript",
            },
            status: "unresolved",
            culprit: "app.js in processData",
            type: "error",
            platform: "javascript",
          });
        },
      ),
      http.get(
        "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CONTEXT-001/events/latest/",
        () => {
          return HttpResponse.json(eventWithContext);
        },
      ),
    );

    const result = await getIssueDetails.handler(
      {
        organizationSlug: "sentry-mcp-evals",
        issueId: "CONTEXT-001",
        eventId: undefined,
        issueUrl: undefined,
        regionUrl: undefined,
      },
      {
        constraints: {
          organizationSlug: null,
        },
        accessToken: "access-token",
        userId: "1",
      },
    );

    // Verify the context (extra) data is displayed
    expect(result).toContain("### Extra Data");
    expect(result).toContain("Additional data attached to this event");
    expect(result).toContain('**custom_field**: "custom_value"');
    expect(result).toContain('**user_action**: "submit_form"');
    expect(result).toContain("**session_data**:");
    expect(result).toContain('"session_id": "sess_12345"');
    expect(result).toContain('"user_id": "user_67890"');
    expect(result).toContain('**environment_info**: "production"');
    // Verify contexts are still displayed
    expect(result).toContain("### Additional Context");
  });
});

```

--------------------------------------------------------------------------------
/packages/mcp-server-mocks/src/fixtures/autofix-state.json:
--------------------------------------------------------------------------------

```json
{
  "autofix": {
    "run_id": 21831,
    "request": {
      "project_id": 4505138086019073
    },
    "status": "COMPLETED",
    "updated_at": "2025-04-09T22:39:50.778146",
    "steps": [
      {
        "active_comment_thread": null,
        "agent_comment_thread": null,
        "completedMessage": null,
        "id": "5c3238ea-4c3a-4c02-a94b-92a3ca25c946",
        "index": 0,
        "initial_memory_length": 1,
        "insights": [
          {
            "change_diff": null,
            "generated_at_memory_index": 0,
            "insight": "The `bottleById` query fails because the input ID (3216) doesn't exist in the database.\n",
            "justification": "The exception details show that the `input` value at the time of the `TRPCError` in `bottleById.ts` was 3216, and the query likely failed because a bottle with ID 3216 was not found in the database.\n\n```\nVariable values at the time of the exception::\n{\n  \"input\": 3216\n}\n```\n",
            "type": "insight"
          },
          {
            "change_diff": null,
            "generated_at_memory_index": 22,
            "insight": "However, the request also includes a different ID (16720) for `bottlePriceList`.\n",
            "justification": "The root cause is likely a mismatch of input IDs within the batched TRPC request, where `bottlePriceList` expects bottle ID 16720, but `bottleById` receives a different ID (3216) leading to the \"Bottle not found\" error.\n\n```\nGET http://api.peated.com/trpc/bottlePriceList,bottleById\n```\n\n```json\n{\n  \"input\": 3216\n}\n```\n\n```\nTRPCError: Bottle not found. (occurred in: GET /trpc/bottlePriceList,bottleById)\n```\n",
            "type": "insight"
          },
          {
            "change_diff": null,
            "generated_at_memory_index": 22,
            "insight": "This suggests a data consistency issue or incorrect client-side request.\n",
            "justification": "The `TRPCError` originates from `bottleById.ts` with the input value being `3216`, indicating the procedure failed to find a bottle with that specific ID in the database.\n\n```\n <anonymous> in file /app/apps/server/src/trpc/routes/bottleById.ts [Line 33, column 13] (In app)\n      .select({\n        ...getTableColumns(bottles),\n      })\n      .from(bottleTombstones)\n      .innerJoin(bottles, eq(bottleTombstones.newBottleId, bottles.id))\n      .where(eq(bottleTombstones.bottleId, input));\n    if (!bottle) {\n      throw new TRPCError({  <-- SUSPECT LINE\n        message: \"Bottle not found.\",\n        code: \"NOT_FOUND\",\n      });\n    }\n  }\n\n  const createdBy = await db.query.users.findFirst({\n---\nVariable values at the time of the exception::\n{\n  \"input\": 3216\n}\n```\n",
            "type": "insight"
          }
        ],
        "key": "root_cause_analysis_processing",
        "output_confidence_score": null,
        "output_stream": "Let me summarize what I know about this issue:\n\n1. The error occurs in the `bottleById.ts` endpoint, with the error \"Bottle not found.\"\n2. The request is to `/trpc/bottlePriceList,bottleById` - which suggests that two TRPC procedures are being called in the same request.\n3. According to the error trace, the input ID is 16720, but the exception details show 3216 as the input value.\n4. The code in `bottleById.ts` tries to look up a bottle in two places:\n   - First in the main `bottles` table\n   - Then in the `bottleTombstones` table (which handles redirects for bottles that have been merged/moved)\n\nThe most significant clue is the discrepancy between the ID mentioned in the error (16720) and the ID in the exception details (3216). This suggests there's something fishy going on with how the IDs are being processed.\n\nGiven that the URL shows `bottlePriceList,bottleById`, this suggests a batched TRPC request where two procedures are being called simultaneously. The batched nature of TRPC calls might be relevant here.\n\nLet's think about potential root causes:\n\n1. **Data problem**: Neither bottle ID 16720 nor 3216 exists in the database.\n2. **Race condition**: The bottle might have been deleted between the two calls.\n3. **TRPC batching issue**: There might be a problem with how the batched request is handling the different IDs for the two procedures.\n4. **Input switching**: The ID is somehow being switched between the procedures.\n\nBased on the evidence, I think the most likely explanation is related to the TRPC batching. When the client makes a request to `/trpc/bottlePriceList,bottleById`, it's calling two procedures:\n- `bottlePriceList` with one ID (maybe 16720)\n- `bottleById` with another ID (3216)\n\nThe error is occurring in the `bottleById` procedure with ID 3216, but the overall request is being reported with ID 16720 (which might be from the `bottlePriceList` part of the batch).\n\nSince the exception is specifically coming from `bottleById.ts` with input 3216, the root cause is that there is no bottle with ID 3216 in the database (neither in `bottles` nor in `bottleTombstones`).\n\nThe confusing part is that the error message mentions 16720, which might be:\n1. A UI issue where the ID displayed doesn't match what's being requested\n2. A client-side issue where the wrong ID is being provided to `bottleById`\n3. A batching issue where the ID from one procedure is being displayed in the error for another\n\nGiven this analysis, the true root cause is: **The `bottleById` procedure is being called with ID 3216, but no bottle with that ID exists in the database.**\n\nThis is definitely a data consistency issue - either the bottle with ID 3216 was deleted without creating a tombstone, or the client is requesting a non-existent ID.# Root Cause Analysis: TRPCError: Bottle not found\n\nAfter examining all the available evidence, I can identify the true root cause of this issue:\n\n## Root Cause\n\n**The issue is related to a TRPC batch request inconsistency between the requested bottle ID and what's being processed.**\n\nThis conclusion is based on several key observations:\n\n1. The request is made to `/trpc/bottlePriceList,bottleById` - this is a batched TRPC request calling two procedures simultaneously.\n\n2. **Critical discrepancy**: The error message references bottle ID 16720, but the exception details show the procedure was actually processing ID 3216.\n\n3. Both procedures (`bottleById` and `bottlePriceList`) contain similar \"Bottle not found\" error checks, but the one that's triggering is in `bottleById.ts`.\n\n4. The `bottleById` procedure is attempting to find bottle ID 3216, not finding it in the main bottles table, then failing to find it in the tombstones table as well.\n\n## Detailed Explanation\n\nWhat's happening is:\n\n1. The client makes a batched request to both `bottlePriceList` and `bottleById` endpoints.\n\n2. Based on the URL and error, it appears that:\n   - The `bottlePriceList` procedure might be receiving ID 16720\n   - The `bottleById` procedure is receiving ID 3216\n\n3. When `bottleById` tries to process ID 3216, it can't find this bottle in the database or in tombstones, so it throws the \"Bottle not found\" error.\n\n4. The error reporting is showing the ID from the overall request (16720) while the actual failure is happening with a different ID (3216).\n\n## Why This Is Happening\n\nThis is likely either:\n\n1. **Client-side issue**: The client is passing the wrong ID to `bottleById` in the batched request\n2. **Data integrity issue**: Bottle ID 3216 may have been deleted without creating a tombstone entry\n3. **Parameter mixing**: The batched TRPC request might be incorrectly mapping parameters between the two procedures\n\nThe fact that two different IDs are involved (16720 and 3216) strongly suggests this is not a simple case of a missing bottle, but rather a data inconsistency or parameter handling issue in the batched request.\n\nTo fix this issue, you would need to:\n1. Check if bottle ID 3216 should exist in the database\n2. Verify how the client is forming the batched request and what IDs it's sending to each procedure\n3. Consider adding more detailed logging that shows the exact inputs to each procedure in batched requests",
        "proceed_confidence_score": null,
        "progress": [
          {
            "data": null,
            "message": "Figuring out the root cause...",
            "timestamp": "2025-04-09T22:35:43.590244",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/trpc/routes/bottleById.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:35:52.808244",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/db/schema.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:36:14.424666",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Searching files with `find apps/server/src/db -type f -name \"*.ts\"`...",
            "timestamp": "2025-04-09T22:36:16.500156",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/db/schema/index.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:36:18.687233",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/db/schema/bottles.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:36:21.205568",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/db/index.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:36:24.013172",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/db/schema/storePrices.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:36:27.236327",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/db/schema/stores.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:36:29.450102",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/db/schema/tastings.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:36:31.593538",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Searching for \"Get information about the bottle price list controller or route that might be calling bottleById.ts\"...",
            "timestamp": "2025-04-09T22:36:34.373043",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Simulating profound thought...",
            "timestamp": "2025-04-09T22:36:56.212382",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Arranging data in a way that looks intentional...",
            "timestamp": "2025-04-09T22:37:33.353804",
            "type": "INFO"
          }
        ],
        "queued_user_messages": [],
        "status": "COMPLETED",
        "title": "Analyzing the Issue",
        "type": "default"
      },
      {
        "active_comment_thread": null,
        "agent_comment_thread": null,
        "causes": [
          {
            "description": "Batched TRPC request incorrectly passed bottle ID 3216 to `bottleById`, instead of 16720, resulting in a \"Bottle not found\" error.",
            "id": 0,
            "root_cause_reproduction": [
              {
                "code_snippet_and_analysis": "This is the entry point where the client requests data from two different procedures in a single HTTP request. The server needs to correctly route and process the parameters for each procedure.",
                "is_most_important_event": false,
                "relevant_code_file": null,
                "timeline_item_type": "human_action",
                "title": "The client initiates a batched TRPC request to the `/trpc/bottlePriceList,bottleById` endpoint, intending to fetch both the price list and details for a specific bottle."
              },
              {
                "code_snippet_and_analysis": "```typescript\n// apps/server/src/trpc/routes/bottlePriceList.ts\n.input(z.object({ bottle: z.number(), onlyValid: z.boolean().optional() }))\n.query(async function ({ input, ctx }) {\n  const [bottle] = await db.select().from(bottles).where(eq(bottles.id, input.bottle));\n  if (!bottle) { ... }\n```\nThis procedure expects a 'bottle' parameter in the input, which is used to query the database.",
                "is_most_important_event": false,
                "relevant_code_file": {
                  "file_path": "apps/server/src/trpc/routes/bottlePriceList.ts",
                  "repo_name": "dcramer/peated"
                },
                "timeline_item_type": "internal_code",
                "title": "The TRPC server receives the batched request and begins processing the `bottlePriceList` procedure, intending to fetch the price list for bottle ID 16720."
              },
              {
                "code_snippet_and_analysis": "```typescript\n// apps/server/src/trpc/routes/bottleById.ts\nexport default publicProcedure.input(z.number()).query(async function ({ input, ctx }) {\n  let [bottle] = await db.select().from(bottles).where(eq(bottles.id, input));\n  if (!bottle) { ... }\n```\nThis procedure expects a numerical ID as input to find the bottle.",
                "is_most_important_event": true,
                "relevant_code_file": {
                  "file_path": "apps/server/src/trpc/routes/bottleById.ts",
                  "repo_name": "dcramer/peated"
                },
                "timeline_item_type": "internal_code",
                "title": "The TRPC server also processes the `bottleById` procedure, but due to a parameter mapping issue or client-side error, it receives bottle ID 3216 as input instead of 16720."
              },
              {
                "code_snippet_and_analysis": "The database query returns no results because bottle ID 3216 is not present in the `bottles` table.",
                "is_most_important_event": false,
                "relevant_code_file": {
                  "file_path": "apps/server/src/trpc/routes/bottleById.ts",
                  "repo_name": "dcramer/peated"
                },
                "timeline_item_type": "external_system",
                "title": "The `bottleById` procedure queries the `bottles` table for a bottle with ID 3216, but no such bottle exists."
              },
              {
                "code_snippet_and_analysis": "The query to `bottleTombstones` also returns no results, indicating that bottle ID 3216 has not been redirected.",
                "is_most_important_event": false,
                "relevant_code_file": {
                  "file_path": "apps/server/src/trpc/routes/bottleById.ts",
                  "repo_name": "dcramer/peated"
                },
                "timeline_item_type": "external_system",
                "title": "The `bottleById` procedure then checks the `bottleTombstones` table to see if bottle ID 3216 has been tombstoned (redirected to a new ID), but no such tombstone exists."
              },
              {
                "code_snippet_and_analysis": "```typescript\n// apps/server/src/trpc/routes/bottleById.ts\nif (!bottle) {\n  throw new TRPCError({ message: \"Bottle not found.\", code: \"NOT_FOUND\" });\n}\n```\nThis is where the error is thrown, indicating that the bottle could not be found.",
                "is_most_important_event": false,
                "relevant_code_file": {
                  "file_path": "apps/server/src/trpc/routes/bottleById.ts",
                  "repo_name": "dcramer/peated"
                },
                "timeline_item_type": "internal_code",
                "title": "Since the `bottleById` procedure cannot find a bottle with ID 3216 in either the `bottles` or `bottleTombstones` tables, it throws a `TRPCError` with the message \"Bottle not found.\""
              }
            ]
          }
        ],
        "completedMessage": null,
        "id": "39166714-b14d-4fa0-a122-3ac241f7b46a",
        "index": 1,
        "key": "root_cause_analysis",
        "output_confidence_score": 0.95,
        "output_stream": null,
        "proceed_confidence_score": 0.9,
        "progress": [
          {
            "data": null,
            "message": "Here is Seer's proposed root cause.",
            "timestamp": "2025-04-09T22:37:40.934397",
            "type": "INFO"
          }
        ],
        "queued_user_messages": [],
        "selection": { "cause_id": 0, "instruction": null },
        "status": "COMPLETED",
        "termination_reason": null,
        "title": "Root Cause Analysis",
        "type": "root_cause_analysis"
      },
      {
        "active_comment_thread": null,
        "agent_comment_thread": null,
        "completedMessage": null,
        "id": "649acf61-9922-4ac4-925d-9b73302ac8e9",
        "index": 2,
        "initial_memory_length": 5,
        "insights": [
          {
            "change_diff": null,
            "generated_at_memory_index": 4,
            "insight": "The discrepancy between inputs suggests a potential issue with batch request handling.\n",
            "justification": "The `TRPCError` occurred in `bottleById.ts` with an input of `3216`, while the request was initiated with a bottle ID of `16720`, indicating a mismatch in the bottle ID being processed.\n\n```\nVariable values at the time of the exception::\n{\n  \"input\": 3216\n}\n```\n\n```\nGET http://api.peated.com/trpc/bottlePriceList,bottleById\n```\n",
            "type": "insight"
          },
          {
            "change_diff": null,
            "generated_at_memory_index": 44,
            "insight": "The client-side batch request construction is the most likely source of error.\n",
            "justification": "The discrepancy between the bottle ID in the error message (16720) and the ID being processed (3216) strongly suggests a client-side parameter mapping issue in the batched TRPC request.\n\nTo fix this, I propose the following steps:\n\n1.  **Inspect the Client-Side Code:** Identify the code responsible for constructing the TRPC batch request to `/trpc/bottlePriceList,bottleById`.\n\n2.  **Verify Parameter Mapping:** Ensure that both `bottlePriceList` and `bottleById` are receiving the correct bottle ID (which should be 16720 in this case).\n\n3.  **Correct Parameter Assignment:** If the parameters are being incorrectly assigned, modify the client-side code to ensure that both procedures receive the same, correct bottle ID.\n\n4.  **Add Logging (Client-Side):** Add temporary logging to the client-side code to confirm the parameters being sent to each procedure in the batch request. This will help verify the fix.\n\n    ```typescript\n    // Example logging (add to the client-side code where the batch request is created)\n    console.log(\"bottlePriceList input:\", { bottle: 16720 }); // Replace 16720 with the actual ID\n    console.log(\"bottleById input:\", { bottle: 16720 }); // Replace 16720 with the actual ID\n    ```\n\n5.  **Test the Fix:** After applying the fix, test the affected functionality to ensure that the \"Bottle not found\" error is resolved and that both procedures are working correctly.\n\n6.  **Remove Logging:** Once the fix is verified, remove the temporary logging.\n\nThis approach directly addresses the most likely cause of the issue and provides a clear path to resolution.\n",
            "type": "insight"
          },
          {
            "change_diff": null,
            "generated_at_memory_index": 44,
            "insight": "Ensuring consistent bottle IDs will likely resolve this inconsistency.\n",
            "justification": "The error message references bottle ID 16720, but the exception details show the procedure was actually processing ID 3216, indicating a parameter mismatch in the batch request.\n\nTo fix this, I recommend the following steps:\n\n1.  **Client-Side Investigation**:\n\n    *   Examine the client-side code where the TRPC batch request is constructed.\n    *   Verify that the same `bottleId` parameter is being passed to both `bottlePriceList` and `bottleById` procedures.\n    *   If the IDs are being passed correctly, inspect the network request payload to confirm the correct structure of the batched request.\n\n    ```typescript\n    // Example (Hypothetical) Client-Side Code\n    const bottleId = 16720; // Example bottle ID\n\n    // Ensure both procedures receive the same bottleId\n    const [priceList, bottleDetails] = await trpc.batch(() => [\n      trpc.bottlePriceList.fetch({ bottle: bottleId }),\n      trpc.bottleById.fetch(bottleId),\n    ]);\n    ```\n\n2.  **Server-Side Logging (Temporary)**:\n\n    *   Add temporary logging to both `bottlePriceList` and `bottleById` procedures to log the received `input` value.\n    *   This will help confirm whether the server is receiving the correct IDs from the client.\n    *   **Important**: Remove these logs after debugging to avoid unnecessary overhead.\n\n    ```typescript\n    // apps/server/src/trpc/routes/bottlePriceList.ts\n    export default publicProcedure\n      .input(\n        z.object({\n          bottle: z.number(),\n          onlyValid: z.boolean().optional(),\n        }),\n      )\n      .query(async function ({ input, ctx }) {\n        console.log(\"bottlePriceList input:\", input); // Add this line\n        // ... rest of the code\n      });\n\n    // apps/server/src/trpc/routes/bottleById.ts\n    export default publicProcedure.input(z.number()).query(async function ({\n      input,\n      ctx,\n    }) {\n      console.log(\"bottleById input:\", input); // Add this line\n      // ... rest of the code\n    });\n    ```\n\n3.  **TRPC Batch Request Configuration**:\n\n    *   Review the TRPC batch link configuration on the client-side.\n    *   Ensure that the batching logic is correctly mapping parameters to the corresponding procedures.\n    *   If using a custom batching implementation, verify its correctness.\n\n4.  **Data Integrity Check**:\n\n    *   If the client-side code appears correct, investigate whether bottle ID 3216 should exist in the database.\n    *   Check the `bottles` table and `bottleTombstones` table for any entries related to bottle ID 3216.\n    *   If the bottle should exist but is missing, investigate potential data deletion or migration issues.\n\n5.  **Tombstone Logic**:\n\n    *   Double-check the logic for creating and using tombstones.\n    *   Ensure that when a bottle is deleted, a tombstone entry is created correctly, pointing to the new bottle (if any).\n\n6.  **Error Handling**:\n\n    *   While this isn't the primary fix, consider improving the error message in `bottleById.ts` to include more context.\n    *   Include the original requested bottle ID (if available) in the error message to aid debugging.\n\n7.  **Client-Side Retries**:\n\n    *   Implement a retry mechanism on the client-side for TRPC requests.\n    *   If a \"Bottle not found\" error occurs, retry the request a few times before giving up. This can help mitigate transient issues.\n\nBy following these steps, you should be able to identify the root cause of the parameter mismatch and implement a fix that ensures consistent bottle IDs are passed to both TRPC procedures in the batch request.\n",
            "type": "insight"
          }
        ],
        "key": "solution_processing",
        "output_confidence_score": null,
        "output_stream": "Based on all the information I've gathered, I'm going to develop a clear fix for this issue. Let's summarize the core problem first:\n\n1. We have a TRPC batch request that calls both `bottlePriceList` and `bottleById` procedures in a single HTTP request.\n2. The error shows a discrepancy between the bottle ID in the error message (16720) and the ID being processed when the error occurred (3216).\n3. The `bottleById` procedure fails because it's trying to find bottle ID 3216, which doesn't exist in the database or in tombstones.\n\nThe most likely issue is a parameter mismatch in the batch TRPC request. When examining the client code and the trace, it seems like the client-side code is sending different bottle IDs to the two procedures incorrectly.\n\nLet's analyze the most probable causes and their solutions:\n\n1. **Client-side parameter mismatch**: The client might be configuring the batch request incorrectly, sending bottle ID 16720 to `bottlePriceList` and bottle ID 3216 to `bottleById`.\n\n2. **Server-side parameter confusion**: There could be some issue with how parameters are being mapped in batched TRPC requests on the server side.\n\n3. **Application-level logic issue**: The application might be assuming that both procedures should operate on the same bottle ID, but this assumption is being violated during batch requests.\n\nFrom the code snippets I've seen, the most likely issue is in the client-side code where the batch request is being constructed. The solution should ensure consistency in parameters across the batch request.\n\nSince we can see from traces that the client is attempting to fetch bottle details, and the frontend code shows that the `bottlePriceList` and `bottleById` are supposed to operate on the same bottle ID, I'm going to recommend a solution that ensures parameter consistency.\n\nThe most actionable and effective fix would be to ensure that both procedures receive the same bottle ID in the batch request. Let's outline that solution in detail:# Solution to Fix the \"Bottle not found\" TRPC Error\n\n## Root Cause\nThe issue stems from a parameter mismatch in TRPC batch requests. When a request is made to `/trpc/bottlePriceList,bottleById`, the procedures are receiving different bottle IDs:\n- `bottlePriceList` receives ID **16720**\n- `bottleById` receives ID **3216**\n\nThe error occurs because bottle ID **3216** doesn't exist in the database.\n\n## Solution Steps\n\n### 1. Fix the Client-Side Batch Request Configuration\n\nThe most direct solution is to ensure parameter consistency in the batch request. Based on the web app's bottle page implementation, both procedures should receive the same bottle ID.\n\n```typescript\n// In apps/web/src/app/(default)/bottles/[bottleId]/(tabs)/page.tsx\nexport default async function BottleDetails({\n  params: { bottleId },\n}: {\n  params: { bottleId: string };\n}) {\n  const trpcClient = await getTrpcClient();\n  \n  // PROBLEM: These are being called separately, potentially with different batching behavior\n  const bottle = await trpcClient.bottleById.fetch(Number(bottleId));\n  \n  // ...rest of the function\n}\n\n// In apps/web/src/app/(default)/bottles/[bottleId]/(tabs)/prices/page.tsx\nexport default async function BottlePrices({\n  params: { bottleId },\n}: {\n  params: { bottleId: string };\n}) {\n  const trpcClient = await getTrpcClient();\n  const priceList = await trpcClient.bottlePriceList.fetch({\n    bottle: Number(bottleId),\n  });\n  \n  // ...rest of the function\n}\n```\n\n### 2. Implement Proper Error Handling for Missing Bottles\n\nAdd graceful handling when a bottle isn't found, especially in the context of batch requests:\n\n```typescript\n// In apps/server/src/trpc/routes/bottleById.ts\nexport default publicProcedure.input(z.number()).query(async function ({\n  input,\n  ctx,\n}) {\n  try {\n    let [bottle] = await db.select().from(bottles).where(eq(bottles.id, input));\n\n    if (!bottle) {\n      // check for a tombstone\n      [bottle] = await db\n        .select({\n          ...getTableColumns(bottles),\n        })\n        .from(bottleTombstones)\n        .innerJoin(bottles, eq(bottleTombstones.newBottleId, bottles.id))\n        .where(eq(bottleTombstones.bottleId, input));\n      if (!bottle) {\n        throw new TRPCError({\n          message: `Bottle not found with ID: ${input}`,  // Include ID in error for better debugging\n          code: \"NOT_FOUND\",\n        });\n      }\n    }\n    \n    // Rest of the function...\n  } catch (error) {\n    // Log the error with context\n    console.error(`Error fetching bottle ID ${input}:`, error);\n    throw error;\n  }\n});\n```\n\n### 3. Update the Batch Request Implementation to Ensure Parameter Consistency\n\nThe most thorough solution is to modify how pages that need both bottle details and price data fetch their information:\n\n```typescript\n// In a shared utility function or component\nexport async function getBottleWithPrices(bottleId: number) {\n  const trpcClient = await getTrpcClient();\n  \n  // Use Promise.all to ensure both requests are part of the same batch\n  // and receive the same parameters\n  const [bottle, priceList] = await Promise.all([\n    trpcClient.bottleById.fetch(bottleId),\n    trpcClient.bottlePriceList.fetch({ bottle: bottleId }),\n  ]);\n  \n  return { bottle, priceList };\n}\n\n// Then in the page components:\nconst { bottle, priceList } = await getBottleWithPrices(Number(bottleId));\n```\n\n### 4. Add Debugging to TRPC Batch Requests\n\nTo prevent similar issues in the future, add better logging for TRPC batch requests:\n\n```typescript\n// In apps/server/src/trpc/index.ts\nexport const publicProcedure = t.procedure.use(sentryMiddleware).use(\n  t.middleware(async ({ ctx, next, path, input }) => {\n    // Log the procedure name and input for debugging\n    console.log(`TRPC procedure: ${path}, input:`, input);\n    \n    return next({\n      ctx,\n    });\n  })\n);\n```\n\n## Implementation Priority\n\n1. **First implement solution #3** - Update how batch requests are made to ensure parameter consistency\n2. **Then implement solution #2** - Improve error messaging and handling\n3. **Consider solution #4** for monitoring and preventing future issues\n\nThis approach addresses the immediate problem while also improving the robustness of the application against similar issues in the future.",
        "proceed_confidence_score": null,
        "progress": [
          {
            "data": null,
            "message": "Figuring out a solution...",
            "timestamp": "2025-04-09T22:37:41.090125",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/db/schema.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:37:58.226774",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Searching files with `find . -name \"schema.ts\" -o -name \"*schema*.ts\"`...",
            "timestamp": "2025-04-09T22:38:06.650338",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Searching for \"database schema definition for bottles and bottleTombstones\"...",
            "timestamp": "2025-04-09T22:38:09.204410",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/db/schema/storePrices.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:38:12.727565",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Searching for \"database schema for storePrices and externalSites\"...",
            "timestamp": "2025-04-09T22:38:14.892955",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Searching for \"schema definition for storePrices table\"...",
            "timestamp": "2025-04-09T22:38:18.078667",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Grepping codebase with `grep -r \"public_trpc\" --include=\"*.ts\" --include=\"*.tsx\"`...",
            "timestamp": "2025-04-09T22:38:22.072610",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Grepping codebase with `grep -r \"createTRPCRouter\" --include=\"*.ts\" --include=\"*.tsx\"`...",
            "timestamp": "2025-04-09T22:38:24.640654",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Searching for \"TRPC router definition or main TRPC setup\"...",
            "timestamp": "2025-04-09T22:38:27.296050",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/trpc/index.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:38:31.342500",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Grepping codebase with `grep -r \"SELECT.*FROM bottle\" --include=\"*.ts\" --include=\"*.tsx\"`...",
            "timestamp": "2025-04-09T22:38:33.896598",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/web/src/routes/[regionSlug]/[...bottleSlug].tsx` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:38:36.691936",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Searching files with `find . -path \"*bottle*\" -name \"*.tsx\"`...",
            "timestamp": "2025-04-09T22:38:39.431421",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/web/src/app/(default)/bottles/[bottleId]/(tabs)/prices/page.tsx` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:38:43.030734",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/web/src/app/(default)/bottles/[bottleId]/(tabs)/page.tsx` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:38:45.535641",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/web/src/lib/trpc/client.server.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:38:48.766893",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/trpc/links.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:38:51.786534",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Looking at `apps/server/src/trpc/context.ts` in `dcramer/peated`...",
            "timestamp": "2025-04-09T22:38:54.281514",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Studying spans under `http.server - GET /trpc/bottlePriceList,bottleById`...",
            "timestamp": "2025-04-09T22:39:00.325683",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Being artificially intelligent...",
            "timestamp": "2025-04-09T22:39:19.353863",
            "type": "INFO"
          },
          {
            "data": null,
            "message": "Formatting for human consumption...",
            "timestamp": "2025-04-09T22:39:47.228661",
            "type": "INFO"
          }
        ],
        "queued_user_messages": [],
        "status": "COMPLETED",
        "title": "Planning Solution",
        "type": "default"
      },
      {
        "active_comment_thread": null,
        "agent_comment_thread": null,
        "completedMessage": null,
        "custom_solution": null,
        "description": "Consolidate bottle and price data fetching into a single batched TRPC request using `Promise.all` to ensure ID consistency.",
        "id": "87ba0ee3-af4e-458e-b3a9-6fd5f9d4a471",
        "index": 3,
        "key": "solution",
        "output_confidence_score": null,
        "output_stream": null,
        "proceed_confidence_score": null,
        "progress": [
          {
            "data": null,
            "message": "Here is Seer's proposed solution.",
            "timestamp": "2025-04-09T22:39:50.191715",
            "type": "INFO"
          }
        ],
        "queued_user_messages": [],
        "selected_mode": null,
        "solution": [
          {
            "code_snippet_and_analysis": "```typescript\n// In a shared utility function or component\nexport async function getBottleWithPrices(bottleId: number) {\n  const trpcClient = await getTrpcClient();\n  \n  // Use Promise.all to ensure both requests are part of the same batch\n  // and receive the same parameters\n  const [bottle, priceList] = await Promise.all([\n    trpcClient.bottleById.fetch(bottleId),\n    trpcClient.bottlePriceList.fetch({ bottle: bottleId }),\n  ]);\n  \n  return { bottle, priceList };\n}\n```\nThis code creates a function that uses `Promise.all` to fetch both bottle details and price data concurrently. This ensures that both TRPC procedures are part of the same batch and receive the same `bottleId`.",
            "is_active": true,
            "is_most_important_event": true,
            "relevant_code_file": null,
            "timeline_item_type": "internal_code",
            "title": "Create a shared utility function to fetch bottle details and price data together."
          },
          {
            "code_snippet_and_analysis": "```typescript\n// Then in the page components:\nconst { bottle, priceList } = await getBottleWithPrices(Number(bottleId));\n```\nThis code replaces the separate calls to `bottleById` and `bottlePriceList` with a single call to the `getBottleWithPrices` function, ensuring that both components receive data for the same bottle.",
            "is_active": true,
            "is_most_important_event": false,
            "relevant_code_file": null,
            "timeline_item_type": "internal_code",
            "title": "Modify the page components to use the shared utility function."
          },
          {
            "code_snippet_and_analysis": null,
            "is_active": false,
            "is_most_important_event": false,
            "relevant_code_file": null,
            "timeline_item_type": "repro_test",
            "title": "Add a unit test that reproduces the issue."
          }
        ],
        "solution_selected": false,
        "status": "COMPLETED",
        "title": "Solution",
        "type": "solution"
      }
    ]
  }
}

```

--------------------------------------------------------------------------------
/packages/mcp-server-mocks/src/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * MSW-based Mock Server for Sentry MCP Development and Testing.
 *
 * Provides comprehensive mock responses for all Sentry API endpoints used by the
 * MCP server. Built with MSW (Mock Service Worker) for realistic HTTP interception
 * and response handling during development and testing.
 *
 * **Usage in Tests:**
 * ```typescript
 * import { mswServer } from "@sentry/mcp-server-mocks";
 *
 * beforeAll(() => mswServer.listen());
 * afterEach(() => mswServer.resetHandlers());
 * afterAll(() => mswServer.close());
 * ```
 *
 * **Usage in Development:**
 * ```typescript
 * // Start mock server for local development
 * mswServer.listen();
 * // Now all Sentry API calls will be intercepted
 * ```
 */
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";

import autofixStateFixture from "./fixtures/autofix-state.json" with {
  type: "json",
};
import issueFixture from "./fixtures/issue.json" with { type: "json" };
import eventsFixture from "./fixtures/event.json" with { type: "json" };
import performanceEventFixture from "./fixtures/performance-event.json" with {
  type: "json",
};
import eventAttachmentsFixture from "./fixtures/event-attachments.json" with {
  type: "json",
};
import tagsFixture from "./fixtures/tags.json" with { type: "json" };
import projectFixture from "./fixtures/project.json" with { type: "json" };
import teamFixture from "./fixtures/team.json" with { type: "json" };
import traceItemsAttributesFixture from "./fixtures/trace-items-attributes.json" with {
  type: "json",
};
import traceItemsAttributesSpansStringFixture from "./fixtures/trace-items-attributes-spans-string.json" with {
  type: "json",
};
import traceItemsAttributesSpansNumberFixture from "./fixtures/trace-items-attributes-spans-number.json" with {
  type: "json",
};
import traceItemsAttributesLogsStringFixture from "./fixtures/trace-items-attributes-logs-string.json" with {
  type: "json",
};
import traceItemsAttributesLogsNumberFixture from "./fixtures/trace-items-attributes-logs-number.json" with {
  type: "json",
};
import traceMetaFixture from "./fixtures/trace-meta.json" with { type: "json" };
import traceMetaWithNullsFixture from "./fixtures/trace-meta-with-nulls.json" with {
  type: "json",
};
import traceFixture from "./fixtures/trace.json" with { type: "json" };
import traceMixedFixture from "./fixtures/trace-mixed.json" with {
  type: "json",
};
import traceEventFixture from "./fixtures/trace-event.json" with {
  type: "json",
};

/**
 * Standard organization payload for mock responses.
 * Used across multiple endpoints for consistency.
 */
const OrganizationPayload = {
  id: "4509106740723712",
  slug: "sentry-mcp-evals",
  name: "sentry-mcp-evals",
  links: {
    regionUrl: "https://us.sentry.io",
    organizationUrl: "https://sentry.io/sentry-mcp-evals",
  },
};

/**
 * Standard release payload for mock responses.
 * Includes typical metadata and project associations.
 */
const ReleasePayload = {
  id: 1402755016,
  version: "8ce89484-0fec-4913-a2cd-e8e2d41dee36",
  status: "open",
  shortVersion: "8ce89484-0fec-4913-a2cd-e8e2d41dee36",
  versionInfo: {
    package: null,
    version: { raw: "8ce89484-0fec-4913-a2cd-e8e2d41dee36" },
    description: "8ce89484-0fec-4913-a2cd-e8e2d41dee36",
    buildHash: null,
  },
  ref: null,
  url: null,
  dateReleased: null,
  dateCreated: "2025-04-13T19:54:21.764000Z",
  data: {},
  newGroups: 0,
  owner: null,
  commitCount: 0,
  lastCommit: null,
  deployCount: 0,
  lastDeploy: null,
  authors: [],
  projects: [
    {
      id: 4509062593708032,
      slug: "cloudflare-mcp",
      name: "cloudflare-mcp",
      newGroups: 0,
      platform: "bun",
      platforms: ["javascript"],
      hasHealthData: false,
    },
  ],
  firstEvent: "2025-04-13T19:54:21Z",
  lastEvent: "2025-04-13T20:28:23Z",
  currentProjectMeta: {},
  userAgent: null,
};

const ClientKeyPayload = {
  id: "d20df0a1ab5031c7f3c7edca9c02814d",
  name: "Default",
  label: "Default",
  public: "d20df0a1ab5031c7f3c7edca9c02814d",
  secret: "154001fd3dfe38130e1c7948a323fad8",
  projectId: 4509109104082945,
  isActive: true,
  rateLimit: null,
  dsn: {
    secret:
      "https://d20df0a1ab5031c7f3c7edca9c02814d:154001fd3dfe38130e1c7948a323fad8@o4509106732793856.ingest.us.sentry.io/4509109104082945",
    public:
      "https://d20df0a1ab5031c7f3c7edca9c02814d@o4509106732793856.ingest.us.sentry.io/4509109104082945",
    csp: "https://o4509106732793856.ingest.us.sentry.io/api/4509109104082945/csp-report/?sentry_key=d20df0a1ab5031c7f3c7edca9c02814d",
    security:
      "https://o4509106732793856.ingest.us.sentry.io/api/4509109104082945/security/?sentry_key=d20df0a1ab5031c7f3c7edca9c02814d",
    minidump:
      "https://o4509106732793856.ingest.us.sentry.io/api/4509109104082945/minidump/?sentry_key=d20df0a1ab5031c7f3c7edca9c02814d",
    nel: "https://o4509106732793856.ingest.us.sentry.io/api/4509109104082945/nel/?sentry_key=d20df0a1ab5031c7f3c7edca9c02814d",
    unreal:
      "https://o4509106732793856.ingest.us.sentry.io/api/4509109104082945/unreal/d20df0a1ab5031c7f3c7edca9c02814d/",
    crons:
      "https://o4509106732793856.ingest.us.sentry.io/api/4509109104082945/cron/___MONITOR_SLUG___/d20df0a1ab5031c7f3c7edca9c02814d/",
    cdn: "https://js.sentry-cdn.com/d20df0a1ab5031c7f3c7edca9c02814d.min.js",
  },
  browserSdkVersion: "8.x",
  browserSdk: {
    choices: [
      ["9.x", "9.x"],
      ["8.x", "8.x"],
      ["7.x", "7.x"],
    ],
  },
  dateCreated: "2025-04-07T00:12:25.139394Z",
  dynamicSdkLoaderOptions: {
    hasReplay: true,
    hasPerformance: true,
    hasDebug: false,
  },
};

// a newer issue, seen less recently
const issueFixture2 = {
  ...issueFixture,
  id: 6507376926,
  shortId: "CLOUDFLARE-MCP-42",
  count: 1,
  title: "Error: Tool list_issues is already registered",
  firstSeen: "2025-04-11T22:51:19.403000Z",
  lastSeen: "2025-04-12T11:34:11Z",
};

const EventsErrorsMeta = {
  fields: {
    "issue.id": "integer",
    title: "string",
    project: "string",
    "count()": "integer",
    "last_seen()": "date",
  },
  units: {
    "issue.id": null,
    title: null,
    project: null,
    "count()": null,
    "last_seen()": null,
  },
  isMetricsData: false,
  isMetricsExtractedData: false,
  tips: { query: null, columns: null },
  datasetReason: "unchanged",
  dataset: "errors",
};

const EmptyEventsErrorsPayload = {
  data: [],
  meta: EventsErrorsMeta,
};

const EventsErrorsPayload = {
  data: [
    {
      "issue.id": 6114575469,
      title: "Error: Tool list_organizations is already registered",
      project: "test-suite",
      "count()": 2,
      "last_seen()": "2025-04-07T12:23:39+00:00",
      issue: "CLOUDFLARE-MCP-41",
    },
  ],
  meta: EventsErrorsMeta,
};

const EventsSpansMeta = {
  fields: {
    id: "string",
    "span.op": "string",
    "span.description": "string",
    "span.duration": "duration",
    transaction: "string",
    timestamp: "string",
    is_transaction: "boolean",
    project: "string",
    trace: "string",
    "transaction.span_id": "string",
    "project.name": "string",
  },
  units: {
    id: null,
    "span.op": null,
    "span.description": null,
    "span.duration": "millisecond",
    transaction: null,
    timestamp: null,
    is_transaction: null,
    project: null,
    trace: null,
    "transaction.span_id": null,
    "project.name": null,
  },
  isMetricsData: false,
  isMetricsExtractedData: false,
  tips: {},
  datasetReason: "unchanged",
  dataset: "spans",
  dataScanned: "full",
  accuracy: {
    confidence: [
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
    ],
  },
};

const EmptyEventsSpansPayload = {
  data: [],
  meta: EventsSpansMeta,
};

const EventsSpansPayload = {
  data: [
    {
      id: "07752c6aeb027c8f",
      "span.op": "http.server",
      "span.description": "GET /trpc/bottleList",
      "span.duration": 12.0,
      transaction: "GET /trpc/bottleList",
      timestamp: "2025-04-13T14:19:18+00:00",
      is_transaction: true,
      project: "peated",
      trace: "6a477f5b0f31ef7b6b9b5e1dea66c91d",
      "transaction.span_id": "07752c6aeb027c8f",
      "project.name": "peated",
    },
    {
      id: "7ab5edf5b3ba42c9",
      "span.op": "http.server",
      "span.description": "GET /trpc/bottleList",
      "span.duration": 18.0,
      transaction: "GET /trpc/bottleList",
      timestamp: "2025-04-13T14:19:17+00:00",
      is_transaction: true,
      project: "peated",
      trace: "54177131c7b192a446124daba3136045",
      "transaction.span_id": "7ab5edf5b3ba42c9",
      "project.name": "peated",
    },
  ],
  meta: EventsSpansMeta,
  confidence: [
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
    {},
  ],
};

/**
 * Builds MSW handlers for both SaaS and self-hosted Sentry instances.
 *
 * Creates handlers based on the controlOnly flag:
 * - controlOnly: false (default) - Creates handlers for both sentry.io and us.sentry.io
 * - controlOnly: true - Creates handlers only for sentry.io (main host)
 *
 * @param handlers - Array of handler definitions with method, path, fetch function, and optional controlOnly flag
 * @returns Array of MSW http handlers
 *
 * @example Handler Definitions
 * ```typescript
 * buildHandlers([
 *   {
 *     method: "get",
 *     path: "/api/0/auth/",
 *     fetch: () => HttpResponse.json({ user: "data" }),
 *     controlOnly: true  // Only available on sentry.io
 *   },
 *   {
 *     method: "get",
 *     path: "/api/0/organizations/",
 *     fetch: () => HttpResponse.json([OrganizationPayload]),
 *     controlOnly: false  // Available on both sentry.io and us.sentry.io
 *   }
 * ]);
 * ```
 */
function buildHandlers(
  handlers: {
    method: keyof typeof http;
    path: string;
    fetch: Parameters<(typeof http)[keyof typeof http]>[1];
    controlOnly?: boolean;
  }[],
) {
  const result = [];

  for (const handler of handlers) {
    // Always add handler for main host (sentry.io)
    result.push(
      http[handler.method](`https://sentry.io${handler.path}`, handler.fetch),
    );

    // Only add handler for region-specific host if not controlOnly
    if (!handler.controlOnly) {
      result.push(
        http[handler.method](
          `https://us.sentry.io${handler.path}`,
          handler.fetch,
        ),
      );
    }
  }

  return result;
}

/**
 * Complete set of Sentry API mock handlers.
 *
 * Covers all endpoints used by the MCP server with realistic responses,
 * parameter validation, and error scenarios.
 */
export const restHandlers = buildHandlers([
  // User data endpoints - controlOnly: true (only available on sentry.io)
  {
    method: "get",
    path: "/api/0/auth/",
    controlOnly: true,
    fetch: () => {
      return HttpResponse.json({
        id: "123456",
        name: "Test User",
        email: "[email protected]",
        username: "testuser",
        avatarUrl: "https://example.com/avatar.jpg",
        dateJoined: "2024-01-01T00:00:00Z",
        isActive: true,
        isManaged: false,
        isStaff: false,
        isSuperuser: false,
        lastLogin: "2024-12-01T00:00:00Z",
        has2fa: false,
        hasPasswordAuth: true,
        emails: [
          {
            id: "1",
            email: "[email protected]",
            is_verified: true,
          },
        ],
      });
    },
  },
  {
    method: "get",
    path: "/api/0/users/me/regions/",
    controlOnly: true,
    fetch: () => {
      return HttpResponse.json({
        regions: [{ name: "us", url: "https://us.sentry.io" }],
      });
    },
  },
  // All other endpoints - controlOnly: false (default, available on both hosts)
  {
    method: "get",
    path: "/api/0/organizations/",
    fetch: () => {
      return HttpResponse.json([OrganizationPayload]);
    },
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/",
    fetch: () => {
      return HttpResponse.json(OrganizationPayload);
    },
  },
  // 404 handlers for test scenarios
  {
    method: "get",
    path: "/api/0/organizations/nonexistent-org/",
    fetch: () => {
      return HttpResponse.json(
        { detail: "The requested resource does not exist" },
        { status: 404 },
      );
    },
  },
  {
    method: "get",
    path: "/api/0/projects/sentry-mcp-evals/nonexistent-project/",
    fetch: () => {
      return HttpResponse.json(
        { detail: "The requested resource does not exist" },
        { status: 404 },
      );
    },
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/teams/",
    fetch: () => {
      return HttpResponse.json([teamFixture]);
    },
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/projects/",
    fetch: () => {
      return HttpResponse.json([
        {
          ...projectFixture,
          id: "4509106749636608", // Different ID for GET endpoint
        },
      ]);
    },
  },
  {
    method: "post",
    path: "/api/0/organizations/sentry-mcp-evals/teams/",
    fetch: () => {
      // TODO: validate payload (only accept 'the-goats' for team name)
      return HttpResponse.json(
        {
          ...teamFixture,
          id: "4509109078196224",
          dateCreated: "2025-04-07T00:05:48.196710Z",
          access: [
            "event:read",
            "org:integrations",
            "org:read",
            "member:read",
            "alerts:write",
            "event:admin",
            "team:admin",
            "project:releases",
            "team:read",
            "project:write",
            "event:write",
            "team:write",
            "project:read",
            "project:admin",
            "alerts:read",
          ],
        },
        { status: 201 },
      );
    },
  },
  {
    method: "post",
    path: "/api/0/teams/sentry-mcp-evals/the-goats/projects/",
    fetch: async ({ request }) => {
      // TODO: validate payload (only accept 'cloudflare-mcp' for project name)
      const body = (await request.json()) as any;
      return HttpResponse.json({
        ...projectFixture,
        name: body?.name || "cloudflare-mcp",
        slug: body?.slug || "cloudflare-mcp",
        platform: body?.platform || "node",
      });
    },
  },
  {
    method: "get",
    path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/",
    fetch: () => {
      return HttpResponse.json(projectFixture);
    },
  },
  {
    method: "put",
    path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/",
    fetch: async ({ request }) => {
      const body = (await request.json()) as any;
      return HttpResponse.json({
        ...projectFixture,
        slug: body?.slug || "cloudflare-mcp",
        name: body?.name || "cloudflare-mcp",
        platform: body?.platform || "node",
      });
    },
  },
  {
    method: "post",
    path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/keys/",
    fetch: () => {
      // TODO: validate payload (only accept 'Default' for key name)
      return HttpResponse.json(ClientKeyPayload);
    },
  },
  {
    method: "get",
    path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/keys/",
    fetch: () => {
      return HttpResponse.json([ClientKeyPayload]);
    },
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/events/",
    fetch: async ({ request }) => {
      const url = new URL(request.url);
      const dataset = url.searchParams.get("dataset");
      const query = url.searchParams.get("query");
      const fields = url.searchParams.getAll("field");

      if (dataset === "spans") {
        //[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
        if (query !== "is_transaction:true") {
          return HttpResponse.json(EmptyEventsSpansPayload);
        }

        if (url.searchParams.get("useRpc") !== "1") {
          return HttpResponse.json("Invalid useRpc", { status: 400 });
        }

        if (
          !fields.includes("id") ||
          !fields.includes("trace") ||
          !fields.includes("span.op") ||
          !fields.includes("span.description") ||
          !fields.includes("span.duration")
        ) {
          return HttpResponse.json("Invalid fields", { status: 400 });
        }
        return HttpResponse.json(EventsSpansPayload);
      }
      if (dataset === "errors") {
        //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=

        if (
          !fields.includes("issue") ||
          !fields.includes("title") ||
          !fields.includes("project") ||
          !fields.includes("last_seen()") ||
          !fields.includes("count()")
        ) {
          return HttpResponse.json("Invalid fields", { status: 400 });
        }

        if (
          !["-count", "-last_seen"].includes(
            url.searchParams.get("sort") as string,
          )
        ) {
          return HttpResponse.json("Invalid sort", { status: 400 });
        }

        // TODO: this is not correct, but itll fix test flakiness for now
        const sortedQuery = query ? query?.split(" ").sort().join(" ") : null;
        if (
          ![
            null,
            "",
            "error.handled:false",
            "error.unhandled:true",
            "error.handled:false is:unresolved",
            "error.unhandled:true is:unresolved",
            "is:unresolved project:cloudflare-mcp",
            "project:cloudflare-mcp",
            "user.email:[email protected]",
          ].includes(sortedQuery)
        ) {
          return HttpResponse.json(EmptyEventsErrorsPayload);
        }

        return HttpResponse.json(EventsErrorsPayload);
      }

      return HttpResponse.json("Invalid dataset", { status: 400 });
    },
  },
  {
    method: "get",
    path: "/api/0/projects/sentry-mcp-evals/foobar/issues/",
    fetch: () => HttpResponse.json([]),
  },
  {
    method: "get",
    path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/issues/",
    fetch: ({ request }) => {
      const url = new URL(request.url);
      const sort = url.searchParams.get("sort");

      if (![null, "user", "freq", "date", "new", null].includes(sort)) {
        return HttpResponse.json(
          `Invalid sort: ${url.searchParams.get("sort")}`,
          {
            status: 400,
          },
        );
      }

      const collapse = url.searchParams.getAll("collapse");
      if (collapse.includes("stats")) {
        return HttpResponse.json(`Invalid collapse: ${collapse.join(",")}`, {
          status: 400,
        });
      }

      const query = url.searchParams.get("query");
      const queryTokens = query?.split(" ").sort() ?? [];
      const sortedQuery = queryTokens ? queryTokens.join(" ") : null;
      if (
        ![
          null,
          "",
          "is:unresolved",
          "error.handled:false is:unresolved",
          "error.unhandled:true is:unresolved",
          "user.email:[email protected]",
        ].includes(sortedQuery)
      ) {
        return HttpResponse.json([]);
      }

      if (queryTokens.includes("user.email:[email protected]")) {
        return HttpResponse.json([issueFixture]);
      }

      if (sort === "date") {
        return HttpResponse.json([issueFixture, issueFixture2]);
      }
      return HttpResponse.json([issueFixture2, issueFixture]);
    },
  },

  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/",
    fetch: ({ request }) => {
      const url = new URL(request.url);
      const sort = url.searchParams.get("sort");

      if (![null, "user", "freq", "date", "new", null].includes(sort)) {
        return HttpResponse.json(
          `Invalid sort: ${url.searchParams.get("sort")}`,
          {
            status: 400,
          },
        );
      }

      const collapse = url.searchParams.getAll("collapse");
      if (collapse.includes("stats")) {
        return HttpResponse.json(`Invalid collapse: ${collapse.join(",")}`, {
          status: 400,
        });
      }

      const query = url.searchParams.get("query");
      const queryTokens = query?.split(" ").sort() ?? [];
      const sortedQuery = queryTokens ? queryTokens.join(" ") : null;
      if (query === "7ca573c0f4814912aaa9bdc77d1a7d51") {
        return HttpResponse.json([issueFixture]);
      }
      if (
        ![
          null,
          "",
          "is:unresolved",
          "error.handled:false is:unresolved",
          "error.unhandled:true is:unresolved",
          "project:cloudflare-mcp",
          "is:unresolved project:cloudflare-mcp",
          "user.email:[email protected]",
        ].includes(sortedQuery)
      ) {
        if (queryTokens.includes("project:remote-mcp")) {
          return HttpResponse.json(
            {
              detail:
                "Invalid query. Project(s) remote-mcp do not exist or are not actively selected.",
            },
            { status: 400 },
          );
        }
        return HttpResponse.json([]);
      }
      if (queryTokens.includes("user.email:[email protected]")) {
        return HttpResponse.json([issueFixture]);
      }

      if (sort === "date") {
        return HttpResponse.json([issueFixture, issueFixture2]);
      }
      return HttpResponse.json([issueFixture2, issueFixture]);
    },
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/",
    fetch: () => HttpResponse.json(issueFixture),
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/6507376925/",
    fetch: () => HttpResponse.json(issueFixture),
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-42/",
    fetch: () => HttpResponse.json(issueFixture2),
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/6507376926/",
    fetch: () => HttpResponse.json(issueFixture2),
  },

  // Trace endpoints
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/trace-meta/a4d1aae7216b47ff8117cf4e09ce9d0a/",
    fetch: () => HttpResponse.json(traceMetaFixture),
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/trace/a4d1aae7216b47ff8117cf4e09ce9d0a/",
    fetch: () => HttpResponse.json(traceFixture),
  },

  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/7ca573c0f4814912aaa9bdc77d1a7d51/",
    fetch: () => HttpResponse.json(eventsFixture),
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/",
    fetch: () => HttpResponse.json(eventsFixture),
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/7ca573c0f4814912aaa9bdc77d1a7d51/",
    fetch: () => HttpResponse.json(eventsFixture),
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/",
    fetch: () => HttpResponse.json(eventsFixture),
  },
  // TODO: event payload should be tweaked to match issue
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-42/events/latest/",
    fetch: () => HttpResponse.json(eventsFixture),
  },
  // TODO: event payload should be tweaked to match issue
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/6507376926/events/latest/",
    fetch: () => HttpResponse.json(eventsFixture),
  },

  // Performance issue with N+1 query detection
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/latest/",
    fetch: () => HttpResponse.json(performanceEventFixture),
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/7890123456/events/latest/",
    fetch: () => HttpResponse.json(performanceEventFixture),
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/a1b2c3d4e5f6789012345678901234567/",
    fetch: () => HttpResponse.json(performanceEventFixture),
  },

  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/releases/",
    fetch: () => HttpResponse.json([ReleasePayload]),
  },
  {
    method: "get",
    path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/releases/",
    fetch: () => HttpResponse.json([ReleasePayload]),
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/tags/",
    fetch: () => HttpResponse.json(tagsFixture),
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/trace-items/attributes/",
    fetch: ({ request }) => {
      const url = new URL(request.url);
      const itemType = url.searchParams.get("itemType");
      const attributeType = url.searchParams.get("attributeType");

      // Validate required parameters
      if (!itemType) {
        return HttpResponse.json(
          { detail: "itemType parameter is required" },
          { status: 400 },
        );
      }

      if (!attributeType) {
        return HttpResponse.json(
          { detail: "attributeType parameter is required" },
          { status: 400 },
        );
      }

      // Validate itemType values (API accepts both singular and plural forms)
      const normalizedItemType = itemType === "spans" ? "span" : itemType;
      if (!["span", "logs"].includes(normalizedItemType)) {
        return HttpResponse.json(
          {
            detail: `Invalid itemType '${itemType}'. Must be 'span' or 'logs'`,
          },
          { status: 400 },
        );
      }

      // Validate attributeType values
      if (!["string", "number"].includes(attributeType)) {
        return HttpResponse.json(
          {
            detail: `Invalid attributeType '${attributeType}'. Must be 'string' or 'number'`,
          },
          { status: 400 },
        );
      }

      // Return appropriate fixture based on parameters
      if (normalizedItemType === "span") {
        if (attributeType === "string") {
          return HttpResponse.json(traceItemsAttributesSpansStringFixture);
        }
        return HttpResponse.json(traceItemsAttributesSpansNumberFixture);
      }
      if (normalizedItemType === "logs") {
        if (attributeType === "string") {
          return HttpResponse.json(traceItemsAttributesLogsStringFixture);
        }
        return HttpResponse.json(traceItemsAttributesLogsNumberFixture);
      }

      // Fallback (should not reach here with valid inputs)
      return HttpResponse.json(traceItemsAttributesFixture);
    },
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/PEATED-A8/autofix/",
    fetch: () => HttpResponse.json(autofixStateFixture),
  },
  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/",
    fetch: () => HttpResponse.json({ autofix: null }),
  },
  {
    method: "post",
    path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-42/autofix/",
    fetch: () => HttpResponse.json({ run_id: 123 }),
  },
  {
    method: "post",
    path: "/api/0/organizations/sentry-mcp-evals/issues/PEATED-A8/autofix/",
    fetch: () => HttpResponse.json({ run_id: 123 }),
  },

  {
    method: "get",
    path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-45/autofix/",
    fetch: () =>
      HttpResponse.json({
        autofix: {
          run_id: 13,
          request: { project_id: 4505138086019073 },
          status: "COMPLETED",
          updated_at: "2025-04-09T22:39:50.778146",
          steps: [
            {
              type: "root_cause_analysis",
              key: "root_cause_analysis",
              index: 0,
              status: "COMPLETED",
              title: "1. **Root Cause Analysis**",
              output_stream: null,
              progress: [],
              description: "The analysis has completed successfully.",
              causes: [
                {
                  description: "The analysis has completed successfully.",
                  id: 1,
                  root_cause_reproduction: [],
                },
              ],
            },
          ],
        },
      }),
  },
  {
    method: "post",
    path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/teams/:teamSlug/",
    fetch: async ({ request, params }) => {
      const body = (await request.json()) as any;
      const teamSlug = params.teamSlug as string;
      return HttpResponse.json({
        ...teamFixture,
        id: "4509109078196224",
        slug: teamSlug,
        name: teamSlug,
        dateCreated: "2025-04-07T00:05:48.196710Z",
      });
    },
  },
  {
    method: "put",
    path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/",
    fetch: async ({ request }) => {
      const body = (await request.json()) as any;
      const updatedIssue = {
        ...issueFixture,
        status: body?.status || issueFixture.status,
        assignedTo: body?.assignedTo || issueFixture.assignedTo,
      };
      return HttpResponse.json(updatedIssue);
    },
  },
  {
    method: "put",
    path: "/api/0/organizations/sentry-mcp-evals/issues/6507376925/",
    fetch: async ({ request }) => {
      const body = (await request.json()) as any;
      const updatedIssue = {
        ...issueFixture,
        status: body?.status || issueFixture.status,
        assignedTo: body?.assignedTo || issueFixture.assignedTo,
      };
      return HttpResponse.json(updatedIssue);
    },
  },
  {
    method: "put",
    path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-42/",
    fetch: async ({ request }) => {
      const body = (await request.json()) as any;
      const updatedIssue = {
        ...issueFixture2,
        status: body?.status || issueFixture2.status,
        assignedTo: body?.assignedTo || issueFixture2.assignedTo,
      };
      return HttpResponse.json(updatedIssue);
    },
  },
  {
    method: "put",
    path: "/api/0/organizations/sentry-mcp-evals/issues/6507376926/",
    fetch: async ({ request }) => {
      const body = (await request.json()) as any;
      const updatedIssue = {
        ...issueFixture2,
        status: body?.status || issueFixture2.status,
        assignedTo: body?.assignedTo || issueFixture2.assignedTo,
      };
      return HttpResponse.json(updatedIssue);
    },
  },
  // Event attachment endpoints
  {
    method: "get",
    path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/events/7ca573c0f4814912aaa9bdc77d1a7d51/attachments/",
    fetch: () => HttpResponse.json(eventAttachmentsFixture),
  },
  {
    method: "get",
    path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/events/7ca573c0f4814912aaa9bdc77d1a7d51/attachments/123/",
    fetch: () => {
      // Mock attachment blob response
      const mockBlob = new Blob(["fake image data"], { type: "image/png" });
      return new HttpResponse(mockBlob, {
        headers: {
          "Content-Type": "image/png",
        },
      });
    },
  },
]);

// Add handlers for mcp.sentry.dev and localhost
export const searchHandlers = [
  http.post("https://mcp.sentry.dev/api/search", async ({ request }) => {
    const body = (await request.json()) as any;

    // Mock different results based on guide
    let results = [
      {
        id: "product/rate-limiting.md",
        url: "https://docs.sentry.io/product/rate-limiting",
        snippet:
          "Learn how to configure rate limiting in Sentry to prevent quota exhaustion and control event ingestion.",
        relevance: 0.95,
      },
      {
        id: "product/accounts/quotas/spike-protection.md",
        url: "https://docs.sentry.io/product/accounts/quotas/spike-protection",
        snippet:
          "Spike protection helps prevent unexpected spikes in event volume from consuming your quota.",
        relevance: 0.87,
      },
    ];

    // If guide is specified, return platform-specific results
    if (body?.guide) {
      const guide = body.guide;
      if (guide.includes("/")) {
        const [platformName, guideName] = guide.split("/");
        results = [
          {
            id: `platforms/${platformName}/guides/${guideName}.md`,
            url: `https://docs.sentry.io/platforms/${platformName}/guides/${guideName}`,
            snippet: `Setup guide for ${guideName} on ${platformName}`,
            relevance: 0.95,
          },
        ];
      } else {
        results = [
          {
            id: `platforms/${guide}/index.md`,
            url: `https://docs.sentry.io/platforms/${guide}`,
            snippet: `Documentation for ${guide} platform`,
            relevance: 0.95,
          },
        ];
      }
    }

    // Return mock search results
    return HttpResponse.json({
      query: body?.query || "",
      results,
    });
  }),
];

// Mock handlers for documentation fetching
export const docsHandlers = [
  http.get("https://docs.sentry.io/product/rate-limiting.md", () => {
    return new HttpResponse(
      `# Project Rate Limits and Quotas

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.

## Why Use Rate Limiting?

- **Cost Control**: Prevent unexpected charges from error spikes
- **Noise Reduction**: Filter out repetitive or low-value events
- **Resource Management**: Ensure critical projects have quota available
- **Performance**: Reduce load on your Sentry organization

## Types of Rate Limits

### 1. Organization Rate Limits

Set a maximum number of events per hour across your entire organization:

\`\`\`python
# In your organization settings
rate_limit = 1000  # events per hour
\`\`\`

### 2. Project Rate Limits

Configure limits for specific projects:

\`\`\`javascript
// Project settings
{
  "rateLimit": {
    "window": 3600,  // 1 hour in seconds
    "limit": 500     // max events
  }
}
\`\`\`

### 3. Key-Based Rate Limiting

Rate limit by specific attributes:

- **By Release**: Limit events from specific releases
- **By User**: Prevent single users from consuming quota
- **By Transaction**: Control high-volume transactions

## Configuration Examples

### SDK Configuration

Configure client-side sampling to reduce events before they're sent:

\`\`\`javascript
Sentry.init({
  dsn: "your-dsn",
  tracesSampleRate: 0.1,  // Sample 10% of transactions
  beforeSend(event) {
    // Custom filtering logic
    if (event.exception?.values?.[0]?.value?.includes("NetworkError")) {
      return null;  // Drop network errors
    }
    return event;
  }
});
\`\`\`

### Inbound Filters

Use Sentry's inbound filters to drop events server-side:

1. Go to **Project Settings** → **Inbound Filters**
2. Enable filters for:
   - Legacy browsers
   - Web crawlers
   - Specific error messages
   - IP addresses

### Spike Protection

Enable spike protection to automatically limit events during traffic spikes:

\`\`\`python
# Project settings
spike_protection = {
  "enabled": True,
  "max_events_per_hour": 10000,
  "detection_window": 300  # 5 minutes
}
\`\`\`

## Best Practices

1. **Start Conservative**: Begin with lower limits and increase as needed
2. **Monitor Usage**: Regularly review your quota consumption
3. **Use Sampling**: Implement transaction sampling for high-volume apps
4. **Filter Noise**: Drop known low-value events at the SDK level
5. **Set Alerts**: Configure notifications for quota thresholds

## Rate Limit Headers

Sentry returns rate limit information in response headers:

\`\`\`
X-Sentry-Rate-Limit: 60
X-Sentry-Rate-Limit-Remaining: 42
X-Sentry-Rate-Limit-Reset: 1634567890
\`\`\`

## Quota Management

### Viewing Quota Usage

1. Navigate to **Settings** → **Subscription**
2. View usage by:
   - Project
   - Event type
   - Time period

### On-Demand Budgets

Purchase additional events when approaching limits:

\`\`\`bash
# Via API
curl -X POST https://sentry.io/api/0/organizations/{org}/quotas/ \\
  -H 'Authorization: Bearer <token>' \\
  -d '{"events": 100000}'
\`\`\`

## Troubleshooting

### Events Being Dropped?

Check:
1. Organization and project rate limits
2. Spike protection status
3. SDK sampling configuration
4. Inbound filter settings

### Rate Limit Errors

If you see 429 errors:
- Review your rate limit configuration
- Implement exponential backoff
- Consider event buffering

## Related Documentation

- [SDK Configuration Guide](/platforms/javascript/configuration)
- [Quotas and Billing](/product/quotas)
- [Filtering Events](/product/data-management/filtering)`,
      {
        headers: {
          "Content-Type": "text/markdown",
        },
      },
    );
  }),
  http.get(
    "https://docs.sentry.io/product/accounts/quotas/spike-protection.md",
    () => {
      return new HttpResponse(
        `# Spike Protection

Spike protection prevents sudden spikes in event volume from consuming your entire quota.

## How it works

When Sentry detects an abnormal spike in events, it automatically activates spike protection...`,
        {
          headers: {
            "Content-Type": "text/markdown",
          },
        },
      );
    },
  ),
  // Catch-all for other doc paths - return 404
  http.get("https://docs.sentry.io/*.md", () => {
    return new HttpResponse(null, { status: 404 });
  }),
];

/**
 * Configured MSW server instance with all Sentry API mock handlers.
 *
 * Ready-to-use mock server for testing and development. Includes all endpoints
 * with realistic data, parameter validation, and error scenarios.
 *
 * @example Test Setup
 * ```typescript
 * import { mswServer } from "@sentry/mcp-server-mocks";
 *
 * beforeAll(() => mswServer.listen({ onUnhandledRequest: 'error' }));
 * afterEach(() => mswServer.resetHandlers());
 * afterAll(() => mswServer.close());
 * ```
 *
 * @example Development Usage
 * ```typescript
 * import { mswServer } from "@sentry/mcp-server-mocks";
 *
 * // Start intercepting requests
 * mswServer.listen();
 *
 * // Your MCP server will now use mock responses
 * const apiService = new SentryApiService({ host: "sentry.io" });
 * const orgs = await apiService.listOrganizations();
 * console.log(orgs); // Returns mock organization data
 * ```
 *
 * @note User Data Endpoint Restrictions
 * The following endpoints are configured with `controlOnly: true` to work ONLY
 * with the main host (sentry.io) and will NOT respond to requests from
 * region-specific hosts (us.sentry.io, de.sentry.io):
 * - `/api/0/auth/` (whoami endpoint)
 * - `/api/0/users/me/regions/` (find_organizations endpoint)
 *
 * This matches the real Sentry API behavior where user data must always be queried
 * from the main API server.
 */
export const mswServer = setupServer(
  ...restHandlers,
  ...searchHandlers,
  ...docsHandlers,
);

// Export fixtures for use in tests
export {
  autofixStateFixture,
  traceMetaFixture,
  traceMetaWithNullsFixture,
  performanceEventFixture,
  traceFixture,
  traceMixedFixture,
  traceEventFixture,
};

// Export utilities for creating mock servers
export { setupMockServer, startMockServer } from "./utils";

```
Page 9/12FirstPrevNextLast