#
tokens: 44727/50000 4/408 files (page 9/11)
lines: off (toggle) GitHub
raw markdown copy
This is page 9 of 11. Use http://codebase.md/getsentry/sentry-mcp?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
│   └── rules
├── .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
│   │   ├── constraint-do-analysis.md
│   │   ├── deployment.md
│   │   ├── mcpagent-architecture.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
│   │   │   │   │       ├── 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-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-agent.ts
│   │   │   │   │   ├── slug-validation.test.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
│   │   │   │   ├── error-handling.ts
│   │   │   │   ├── fetch-utils.test.ts
│   │   │   │   ├── fetch-utils.ts
│   │   │   │   ├── formatting.test.ts
│   │   │   │   ├── formatting.ts
│   │   │   │   ├── issue-helpers.test.ts
│   │   │   │   ├── issue-helpers.ts
│   │   │   │   ├── test-fixtures.ts
│   │   │   │   └── tool-helpers
│   │   │   │       ├── api.test.ts
│   │   │   │       ├── api.ts
│   │   │   │       ├── define.ts
│   │   │   │       ├── enhance-error.ts
│   │   │   │       ├── formatting.ts
│   │   │   │       ├── issue.ts
│   │   │   │       ├── seer.test.ts
│   │   │   │       ├── seer.ts
│   │   │   │       ├── validate-region-url.test.ts
│   │   │   │       └── validate-region-url.ts
│   │   │   ├── permissions.parseScopes.test.ts
│   │   │   ├── permissions.ts
│   │   │   ├── schema.ts
│   │   │   ├── server.ts
│   │   │   ├── telem
│   │   │   │   ├── index.ts
│   │   │   │   ├── logging.ts
│   │   │   │   ├── sentry.test.ts
│   │   │   │   └── sentry.ts
│   │   │   ├── test-setup.ts
│   │   │   ├── test-utils
│   │   │   │   └── context.ts
│   │   │   ├── toolDefinitions.ts
│   │   │   ├── tools
│   │   │   │   ├── analyze-issue-with-seer.test.ts
│   │   │   │   ├── analyze-issue-with-seer.ts
│   │   │   │   ├── create-dsn.test.ts
│   │   │   │   ├── create-dsn.ts
│   │   │   │   ├── create-project.test.ts
│   │   │   │   ├── create-project.ts
│   │   │   │   ├── create-team.test.ts
│   │   │   │   ├── create-team.ts
│   │   │   │   ├── find-dsns.test.ts
│   │   │   │   ├── find-dsns.ts
│   │   │   │   ├── find-organizations.test.ts
│   │   │   │   ├── find-organizations.ts
│   │   │   │   ├── find-projects.test.ts
│   │   │   │   ├── find-projects.ts
│   │   │   │   ├── find-releases.test.ts
│   │   │   │   ├── find-releases.ts
│   │   │   │   ├── find-teams.test.ts
│   │   │   │   ├── find-teams.ts
│   │   │   │   ├── get-doc.test.ts
│   │   │   │   ├── get-doc.ts
│   │   │   │   ├── get-event-attachment.test.ts
│   │   │   │   ├── get-event-attachment.ts
│   │   │   │   ├── get-issue-details.test.ts
│   │   │   │   ├── get-issue-details.ts
│   │   │   │   ├── get-trace-details.test.ts
│   │   │   │   ├── get-trace-details.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── search-docs.test.ts
│   │   │   │   ├── search-docs.ts
│   │   │   │   ├── search-events
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── formatters.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── utils.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── search-events.test.ts
│   │   │   │   ├── search-issues
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── formatters.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── README.md
│   │   │   │   ├── tools.test.ts
│   │   │   │   ├── types.ts
│   │   │   │   ├── update-issue.test.ts
│   │   │   │   ├── update-issue.ts
│   │   │   │   ├── update-project.test.ts
│   │   │   │   ├── update-project.ts
│   │   │   │   ├── whoami.test.ts
│   │   │   │   └── whoami.ts
│   │   │   ├── transports
│   │   │   │   └── stdio.ts
│   │   │   ├── types.ts
│   │   │   ├── utils
│   │   │   │   ├── slug-validation.test.ts
│   │   │   │   ├── slug-validation.ts
│   │   │   │   ├── url-utils.test.ts
│   │   │   │   └── url-utils.ts
│   │   │   └── version.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   ├── mcp-server-evals
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── bin
│   │   │   │   └── start-mock-stdio.ts
│   │   │   ├── evals
│   │   │   │   ├── autofix.eval.ts
│   │   │   │   ├── create-dsn.eval.ts
│   │   │   │   ├── create-project.eval.ts
│   │   │   │   ├── create-team.eval.ts
│   │   │   │   ├── get-issue.eval.ts
│   │   │   │   ├── get-trace-details.eval.ts
│   │   │   │   ├── list-dsns.eval.ts
│   │   │   │   ├── list-issues.eval.ts
│   │   │   │   ├── list-organizations.eval.ts
│   │   │   │   ├── list-projects.eval.ts
│   │   │   │   ├── list-releases.eval.ts
│   │   │   │   ├── list-tags.eval.ts
│   │   │   │   ├── list-teams.eval.ts
│   │   │   │   ├── search-docs.eval.ts
│   │   │   │   ├── search-events-agent.eval.ts
│   │   │   │   ├── search-events.eval.ts
│   │   │   │   ├── search-issues-agent.eval.ts
│   │   │   │   ├── search-issues.eval.ts
│   │   │   │   ├── update-issue.eval.ts
│   │   │   │   ├── update-project.eval.ts
│   │   │   │   └── utils
│   │   │   │       ├── fixtures.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── runner.ts
│   │   │   │       ├── structuredOutputScorer.ts
│   │   │   │       └── toolPredictionScorer.ts
│   │   │   └── setup-env.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── mcp-server-mocks
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── fixtures
│   │   │   │   ├── autofix-state.json
│   │   │   │   ├── event-attachments.json
│   │   │   │   ├── event.json
│   │   │   │   ├── issue.json
│   │   │   │   ├── performance-event.json
│   │   │   │   ├── project.json
│   │   │   │   ├── tags.json
│   │   │   │   ├── team.json
│   │   │   │   ├── trace-event.json
│   │   │   │   ├── trace-items-attributes-logs-number.json
│   │   │   │   ├── trace-items-attributes-logs-string.json
│   │   │   │   ├── trace-items-attributes-spans-number.json
│   │   │   │   ├── trace-items-attributes-spans-string.json
│   │   │   │   ├── trace-items-attributes.json
│   │   │   │   ├── trace-meta-with-nulls.json
│   │   │   │   ├── trace-meta.json
│   │   │   │   ├── trace-mixed.json
│   │   │   │   └── trace.json
│   │   │   ├── index.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── mcp-server-tsconfig
│   │   ├── package.json
│   │   ├── tsconfig.base.json
│   │   └── tsconfig.vite.json
│   ├── mcp-test-client
│   │   ├── .env.test
│   │   ├── .gitignore
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── agent.ts
│   │   │   ├── auth
│   │   │   │   ├── config.ts
│   │   │   │   └── oauth.ts
│   │   │   ├── constants.ts
│   │   │   ├── index.ts
│   │   │   ├── logger.test.ts
│   │   │   ├── logger.ts
│   │   │   ├── mcp-test-client-remote.ts
│   │   │   ├── mcp-test-client.ts
│   │   │   ├── types.ts
│   │   │   └── version.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── smoke-tests
│       ├── package.json
│       ├── src
│       │   └── smoke.test.ts
│       └── vitest.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── scripts
│   └── check-doc-links.mjs
├── turbo.json
└── vitest.workspace.ts
```

# Files

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

```typescript
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";

```

--------------------------------------------------------------------------------
/packages/mcp-server/src/api-client/client.test.ts:
--------------------------------------------------------------------------------

```typescript
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
import { SentryApiService } from "./client";
import { ConfigurationError } from "../errors";

describe("getIssueUrl", () => {
  it("should work with sentry.io", () => {
    const apiService = new SentryApiService({ host: "sentry.io" });
    const result = apiService.getIssueUrl("sentry-mcp", "123456");
    expect(result).toMatchInlineSnapshot(
      `"https://sentry-mcp.sentry.io/issues/123456"`,
    );
  });
  it("should work with self-hosted", () => {
    const apiService = new SentryApiService({ host: "sentry.example.com" });
    const result = apiService.getIssueUrl("sentry-mcp", "123456");
    expect(result).toMatchInlineSnapshot(
      `"https://sentry.example.com/organizations/sentry-mcp/issues/123456"`,
    );
  });
  it("should work with full URL including protocol", () => {
    const apiService = new SentryApiService({
      host: "sentry.example.com",
    });
    const result = apiService.getIssueUrl("sentry-mcp", "123456");
    expect(result).toMatchInlineSnapshot(
      `"https://sentry.example.com/organizations/sentry-mcp/issues/123456"`,
    );
  });
  it("should always use HTTPS protocol", () => {
    const apiService = new SentryApiService({
      host: "localhost:8000",
    });
    const result = apiService.getIssueUrl("sentry-mcp", "123456");
    expect(result).toMatchInlineSnapshot(
      `"https://localhost:8000/organizations/sentry-mcp/issues/123456"`,
    );
  });
  it("should handle regional URLs correctly for SaaS", () => {
    const apiService = new SentryApiService({ host: "us.sentry.io" });
    const result = apiService.getIssueUrl("sentry", "PROJ-THREAD-LEAKS-12");
    // Should use sentry.io, not us.sentry.io for web UI
    expect(result).toEqual(
      "https://sentry.sentry.io/issues/PROJ-THREAD-LEAKS-12",
    );
  });
  it("should handle EU regional URLs correctly for SaaS", () => {
    const apiService = new SentryApiService({ host: "eu.sentry.io" });
    const result = apiService.getIssueUrl("myorg", "PROJECT-456");
    // Should use sentry.io, not eu.sentry.io for web UI
    expect(result).toEqual("https://myorg.sentry.io/issues/PROJECT-456");
  });
});

describe("getTraceUrl", () => {
  it("should work with sentry.io", () => {
    const apiService = new SentryApiService({ host: "sentry.io" });
    const result = apiService.getTraceUrl(
      "sentry-mcp",
      "6a477f5b0f31ef7b6b9b5e1dea66c91d",
    );
    expect(result).toMatchInlineSnapshot(
      `"https://sentry-mcp.sentry.io/explore/traces/trace/6a477f5b0f31ef7b6b9b5e1dea66c91d"`,
    );
  });
  it("should work with self-hosted", () => {
    const apiService = new SentryApiService({ host: "sentry.example.com" });
    const result = apiService.getTraceUrl(
      "sentry-mcp",
      "6a477f5b0f31ef7b6b9b5e1dea66c91d",
    );
    expect(result).toMatchInlineSnapshot(
      `"https://sentry.example.com/organizations/sentry-mcp/explore/traces/trace/6a477f5b0f31ef7b6b9b5e1dea66c91d"`,
    );
  });
  it("should always use HTTPS protocol", () => {
    const apiService = new SentryApiService({
      host: "localhost:8000",
    });
    const result = apiService.getTraceUrl(
      "sentry-mcp",
      "6a477f5b0f31ef7b6b9b5e1dea66c91d",
    );
    expect(result).toMatchInlineSnapshot(
      `"https://localhost:8000/organizations/sentry-mcp/explore/traces/trace/6a477f5b0f31ef7b6b9b5e1dea66c91d"`,
    );
  });
  it("should handle regional URLs correctly for SaaS", () => {
    const apiService = new SentryApiService({ host: "us.sentry.io" });
    const result = apiService.getTraceUrl(
      "sentry",
      "6a477f5b0f31ef7b6b9b5e1dea66c91d",
    );
    // Should use sentry.io, not us.sentry.io for web UI
    expect(result).toEqual(
      "https://sentry.sentry.io/explore/traces/trace/6a477f5b0f31ef7b6b9b5e1dea66c91d",
    );
  });
});

describe("getEventsExplorerUrl", () => {
  it("should work with sentry.io", () => {
    const apiService = new SentryApiService({ host: "sentry.io" });
    const result = apiService.getEventsExplorerUrl(
      "sentry-mcp",
      "level:error AND message:timeout",
    );
    expect(result).toMatchInlineSnapshot(
      `"https://sentry-mcp.sentry.io/explore/traces/?query=level%3Aerror+AND+message%3Atimeout&statsPeriod=24h&table=span"`,
    );
  });
  it("should work with self-hosted", () => {
    const apiService = new SentryApiService({ host: "sentry.example.com" });
    const result = apiService.getEventsExplorerUrl(
      "sentry-mcp",
      "level:error AND message:timeout",
    );
    expect(result).toMatchInlineSnapshot(
      `"https://sentry.example.com/organizations/sentry-mcp/explore/traces/?query=level%3Aerror+AND+message%3Atimeout&statsPeriod=24h&table=span"`,
    );
  });
  it("should include project parameter when provided", () => {
    const apiService = new SentryApiService({ host: "sentry.io" });
    const result = apiService.getEventsExplorerUrl(
      "sentry-mcp",
      "level:error",
      "backend",
    );
    expect(result).toMatchInlineSnapshot(
      `"https://sentry-mcp.sentry.io/explore/traces/?query=level%3Aerror&project=backend&statsPeriod=24h&table=span"`,
    );
  });
  it("should properly encode special characters in query", () => {
    const apiService = new SentryApiService({ host: "sentry.io" });
    const result = apiService.getEventsExplorerUrl(
      "sentry-mcp",
      'message:"database timeout" AND level:error',
    );
    expect(result).toMatchInlineSnapshot(
      `"https://sentry-mcp.sentry.io/explore/traces/?query=message%3A%22database+timeout%22+AND+level%3Aerror&statsPeriod=24h&table=span"`,
    );
  });
  it("should always use HTTPS protocol", () => {
    const apiService = new SentryApiService({
      host: "localhost:8000",
    });
    const result = apiService.getEventsExplorerUrl("sentry-mcp", "level:error");
    expect(result).toMatchInlineSnapshot(
      `"https://localhost:8000/organizations/sentry-mcp/explore/traces/?query=level%3Aerror&statsPeriod=24h&table=span"`,
    );
  });
  it("should handle regional URLs correctly for SaaS", () => {
    const apiService = new SentryApiService({ host: "us.sentry.io" });
    const result = apiService.getEventsExplorerUrl("sentry", "level:error");
    // Should use sentry.io, not us.sentry.io for web UI
    expect(result).toEqual(
      "https://sentry.sentry.io/explore/traces/?query=level%3Aerror&statsPeriod=24h&table=span",
    );
  });

  describe("time parameters", () => {
    it("should use statsPeriod when provided for spans dataset", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });
      const result = apiService.getEventsExplorerUrl(
        "sentry-mcp",
        "level:error",
        undefined, // projectId
        "spans", // dataset
        undefined, // fields
        undefined, // sort
        undefined, // aggregateFunctions
        undefined, // groupByFields
        "7d", // statsPeriod
      );
      expect(result).toContain("statsPeriod=7d");
      expect(result).not.toContain("start=");
      expect(result).not.toContain("end=");
    });

    it("should use start/end when provided for spans dataset", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });
      const result = apiService.getEventsExplorerUrl(
        "sentry-mcp",
        "level:error",
        undefined, // projectId
        "spans", // dataset
        undefined, // fields
        undefined, // sort
        undefined, // aggregateFunctions
        undefined, // groupByFields
        undefined, // statsPeriod
        "2025-07-29T07:00:00", // start
        "2025-07-31T06:59:59", // end
      );
      expect(result).toContain("start=2025-07-29T07%3A00%3A00");
      expect(result).toContain("end=2025-07-31T06%3A59%3A59");
      expect(result).not.toContain("statsPeriod=");
    });

    it("should prefer start/end over statsPeriod when both provided for spans dataset", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });
      const result = apiService.getEventsExplorerUrl(
        "sentry-mcp",
        "level:error",
        undefined, // projectId
        "spans", // dataset
        undefined, // fields
        undefined, // sort
        undefined, // aggregateFunctions
        undefined, // groupByFields
        "7d", // statsPeriod (should be ignored)
        "2025-07-29T07:00:00", // start
        "2025-07-31T06:59:59", // end
      );
      expect(result).toContain("start=2025-07-29T07%3A00%3A00");
      expect(result).toContain("end=2025-07-31T06%3A59%3A59");
      expect(result).not.toContain("statsPeriod=");
    });

    it("should use statsPeriod when provided for errors dataset", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });
      const result = apiService.getEventsExplorerUrl(
        "sentry-mcp",
        "level:error",
        undefined, // projectId
        "errors", // dataset
        undefined, // fields
        undefined, // sort
        undefined, // aggregateFunctions
        undefined, // groupByFields
        "14d", // statsPeriod
      );
      expect(result).toContain("statsPeriod=14d");
      expect(result).not.toContain("start=");
      expect(result).not.toContain("end=");
    });

    it("should use start/end when provided for errors dataset", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });
      const result = apiService.getEventsExplorerUrl(
        "sentry-mcp",
        "level:error",
        undefined, // projectId
        "errors", // dataset
        undefined, // fields
        undefined, // sort
        undefined, // aggregateFunctions
        undefined, // groupByFields
        undefined, // statsPeriod
        "2025-07-29T07:00:00", // start
        "2025-07-31T06:59:59", // end
      );
      expect(result).toContain("start=2025-07-29T07%3A00%3A00");
      expect(result).toContain("end=2025-07-31T06%3A59%3A59");
      expect(result).not.toContain("statsPeriod=");
    });

    it("should default to 24h when no time parameters provided", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });
      const result = apiService.getEventsExplorerUrl(
        "sentry-mcp",
        "level:error",
      );
      expect(result).toContain("statsPeriod=24h");
    });

    it("should handle aggregate queries with time parameters for spans dataset", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });
      const result = apiService.getEventsExplorerUrl(
        "sentry-mcp",
        "",
        "4509062593708032", // projectId
        "spans", // dataset
        [
          "equation|sum(gen_ai.usage.input_tokens) + sum(gen_ai.usage.output_tokens)",
        ], // fields
        "-equation|sum(gen_ai.usage.input_tokens) + sum(gen_ai.usage.output_tokens)", // sort
        [
          "equation|sum(gen_ai.usage.input_tokens) + sum(gen_ai.usage.output_tokens)",
        ], // aggregateFunctions
        [], // groupByFields
        "7d", // statsPeriod
      );
      expect(result).toContain("statsPeriod=7d");
      expect(result).toContain("project=4509062593708032");
      expect(result).toContain("mode=aggregate");
    });
  });
});

describe("network error handling", () => {
  let originalFetch: typeof globalThis.fetch;

  beforeEach(() => {
    originalFetch = globalThis.fetch;
  });

  afterEach(() => {
    globalThis.fetch = originalFetch;
  });

  it("should handle DNS errors with EAI_AGAIN", async () => {
    const dnsError = new Error("fetch failed");
    dnsError.cause = new Error("getaddrinfo EAI_AGAIN https");

    globalThis.fetch = vi.fn().mockRejectedValue(dnsError);

    const apiService = new SentryApiService({
      host: "sentry.io",
      accessToken: "test-token",
    });

    await expect(apiService.getAuthenticatedUser()).rejects.toThrow(
      /DNS temporarily unavailable/,
    );
  });

  it("should handle ENOTFOUND errors", async () => {
    const notFoundError = new Error("fetch failed");
    notFoundError.cause = new Error("getaddrinfo ENOTFOUND invalid.host");

    globalThis.fetch = vi.fn().mockRejectedValue(notFoundError);

    const apiService = new SentryApiService({
      host: "invalid.host",
      accessToken: "test-token",
    });

    await expect(apiService.getAuthenticatedUser()).rejects.toThrow(
      /Hostname not found/,
    );
  });

  it("should handle ECONNREFUSED errors", async () => {
    const refusedError = new Error("fetch failed");
    refusedError.cause = new Error("connect ECONNREFUSED 127.0.0.1:443");

    globalThis.fetch = vi.fn().mockRejectedValue(refusedError);

    const apiService = new SentryApiService({
      host: "localhost",
      accessToken: "test-token",
    });

    await expect(apiService.getAuthenticatedUser()).rejects.toThrow(
      /Connection refused/,
    );
  });

  it("should handle ETIMEDOUT errors", async () => {
    const timeoutError = new Error("fetch failed");
    timeoutError.cause = new Error("connect ETIMEDOUT 192.168.1.1:443");

    globalThis.fetch = vi.fn().mockRejectedValue(timeoutError);

    const apiService = new SentryApiService({
      host: "192.168.1.1",
      accessToken: "test-token",
    });

    await expect(apiService.getAuthenticatedUser()).rejects.toThrow(
      /Connection timed out/,
    );
  });

  it("should handle ECONNRESET errors", async () => {
    const resetError = new Error("fetch failed");
    resetError.cause = new Error("read ECONNRESET");

    globalThis.fetch = vi.fn().mockRejectedValue(resetError);

    const apiService = new SentryApiService({
      host: "sentry.io",
      accessToken: "test-token",
    });

    await expect(apiService.getAuthenticatedUser()).rejects.toThrow(
      /Connection reset/,
    );
  });

  it("should handle generic network errors", async () => {
    const genericError = new Error("Network request failed");

    globalThis.fetch = vi.fn().mockRejectedValue(genericError);

    const apiService = new SentryApiService({
      host: "sentry.io",
      accessToken: "test-token",
    });

    await expect(apiService.getAuthenticatedUser()).rejects.toThrow(
      /Unable to connect to .* - Network request failed/,
    );
  });

  it("should preserve the original error in the cause chain", async () => {
    const originalError = new Error("getaddrinfo EAI_AGAIN");
    const fetchError = new Error("fetch failed");
    fetchError.cause = originalError;

    globalThis.fetch = vi.fn().mockRejectedValue(fetchError);

    const apiService = new SentryApiService({
      host: "sentry.io",
      accessToken: "test-token",
    });

    try {
      await apiService.getAuthenticatedUser();
    } catch (error) {
      expect(error).toBeInstanceOf(Error);
      expect((error as Error).cause).toBe(fetchError);
      expect(((error as Error).cause as Error).cause).toBe(originalError);
    }
  });

  it("should throw ConfigurationError for DNS errors", async () => {
    const dnsError = new Error("fetch failed");
    dnsError.cause = new Error("getaddrinfo ENOTFOUND invalid.host");

    globalThis.fetch = vi.fn().mockRejectedValue(dnsError);

    const apiService = new SentryApiService({
      host: "invalid.host",
      accessToken: "test-token",
    });

    await expect(apiService.getAuthenticatedUser()).rejects.toThrow(
      ConfigurationError,
    );
  });

  it("should throw ConfigurationError for connection timeout errors", async () => {
    const timeoutError = new Error("fetch failed");
    timeoutError.cause = new Error("connect ETIMEDOUT 192.168.1.1:443");

    globalThis.fetch = vi.fn().mockRejectedValue(timeoutError);

    const apiService = new SentryApiService({
      host: "192.168.1.1",
      accessToken: "test-token",
    });

    await expect(apiService.getAuthenticatedUser()).rejects.toThrow(
      ConfigurationError,
    );
  });
});

describe("listOrganizations", () => {
  let originalFetch: typeof globalThis.fetch;

  beforeEach(() => {
    originalFetch = globalThis.fetch;
  });

  afterEach(() => {
    globalThis.fetch = originalFetch;
  });

  it("should fetch from regions endpoint for SaaS", async () => {
    const mockRegionsResponse = {
      regions: [
        { name: "US", url: "https://us.sentry.io" },
        { name: "EU", url: "https://eu.sentry.io" },
      ],
    };

    const mockOrgsUs = [{ id: "1", slug: "org-us", name: "Org US" }];
    const mockOrgsEu = [{ id: "2", slug: "org-eu", name: "Org EU" }];

    let callCount = 0;
    globalThis.fetch = vi.fn().mockImplementation((url: string) => {
      callCount++;
      if (url.includes("/users/me/regions/")) {
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve(mockRegionsResponse),
        });
      }
      if (url.includes("us.sentry.io")) {
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve(mockOrgsUs),
        });
      }
      if (url.includes("eu.sentry.io")) {
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve(mockOrgsEu),
        });
      }
      return Promise.reject(new Error("Unexpected URL"));
    });

    const apiService = new SentryApiService({
      host: "sentry.io",
      accessToken: "test-token",
    });

    const result = await apiService.listOrganizations();

    expect(callCount).toBe(3); // 1 regions call + 2 org calls
    expect(result).toHaveLength(2);
    expect(result).toContainEqual(expect.objectContaining({ slug: "org-us" }));
    expect(result).toContainEqual(expect.objectContaining({ slug: "org-eu" }));
  });

  it("should fetch directly from organizations endpoint for self-hosted", async () => {
    const mockOrgs = [
      { id: "1", slug: "org-1", name: "Organization 1" },
      { id: "2", slug: "org-2", name: "Organization 2" },
    ];

    let callCount = 0;
    globalThis.fetch = vi.fn().mockImplementation((url: string) => {
      callCount++;
      if (url.includes("/organizations/")) {
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve(mockOrgs),
        });
      }
      return Promise.reject(new Error("Unexpected URL"));
    });

    const apiService = new SentryApiService({
      host: "sentry.example.com",
      accessToken: "test-token",
    });

    const result = await apiService.listOrganizations();

    expect(callCount).toBe(1); // Only 1 org call, no regions call
    expect(result).toHaveLength(2);
    expect(result).toEqual(mockOrgs);
    // Verify that regions endpoint was not called
    expect(globalThis.fetch).not.toHaveBeenCalledWith(
      expect.stringContaining("/users/me/regions/"),
      expect.any(Object),
    );
  });

  it("should fall back to direct organizations endpoint when regions endpoint returns 404 on SaaS", async () => {
    const mockOrgs = [
      { id: "1", slug: "org-1", name: "Organization 1" },
      { id: "2", slug: "org-2", name: "Organization 2" },
    ];

    globalThis.fetch = vi.fn().mockImplementation((url: string) => {
      if (url.includes("/users/me/regions/")) {
        return Promise.resolve({
          ok: false,
          status: 404,
          statusText: "Not Found",
          text: () => Promise.resolve(JSON.stringify({ detail: "Not found" })),
        });
      }
      if (url.includes("/organizations/")) {
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve(mockOrgs),
        });
      }
      return Promise.reject(new Error("Unexpected URL"));
    });

    const apiService = new SentryApiService({
      host: "sentry.io",
      accessToken: "test-token",
    });

    const result = await apiService.listOrganizations();

    expect(result).toHaveLength(2);
    expect(result).toEqual(mockOrgs);

    // Verify it tried regions first, then fell back to organizations
    expect(globalThis.fetch).toHaveBeenCalledWith(
      expect.stringContaining("/users/me/regions/"),
      expect.any(Object),
    );
    expect(globalThis.fetch).toHaveBeenCalledWith(
      expect.stringContaining("/organizations/"),
      expect.any(Object),
    );
  });
});

describe("host configuration", () => {
  it("should handle hostname without protocol", () => {
    const apiService = new SentryApiService({ host: "sentry.io" });
    // @ts-expect-error - accessing private property for testing
    expect(apiService.host).toBe("sentry.io");
    // @ts-expect-error - accessing private property for testing
    expect(apiService.apiPrefix).toBe("https://sentry.io/api/0");
  });

  it("should handle hostname with port", () => {
    const apiService = new SentryApiService({ host: "localhost:8000" });
    // @ts-expect-error - accessing private property for testing
    expect(apiService.host).toBe("localhost:8000");
    // @ts-expect-error - accessing private property for testing
    expect(apiService.apiPrefix).toBe("https://localhost:8000/api/0");
  });

  it("should always use HTTPS protocol", () => {
    const apiService = new SentryApiService({
      host: "sentry.example.com",
    });
    // @ts-expect-error - accessing private property for testing
    expect(apiService.host).toBe("sentry.example.com");
    // @ts-expect-error - accessing private property for testing
    expect(apiService.apiPrefix).toBe("https://sentry.example.com/api/0");
  });

  it("should always use HTTPS even for localhost", () => {
    const apiService = new SentryApiService({
      host: "localhost:8000",
    });
    // @ts-expect-error - accessing private property for testing
    expect(apiService.host).toBe("localhost:8000");
    // @ts-expect-error - accessing private property for testing
    expect(apiService.apiPrefix).toBe("https://localhost:8000/api/0");
  });

  it("should update host and API prefix with setHost", () => {
    const apiService = new SentryApiService({ host: "sentry.io" });

    apiService.setHost("eu.sentry.io");
    // @ts-expect-error - accessing private property for testing
    expect(apiService.host).toBe("eu.sentry.io");
    // @ts-expect-error - accessing private property for testing
    expect(apiService.apiPrefix).toBe("https://eu.sentry.io/api/0");

    apiService.setHost("localhost:9000");
    // @ts-expect-error - accessing private property for testing
    expect(apiService.host).toBe("localhost:9000");
    // @ts-expect-error - accessing private property for testing
    expect(apiService.apiPrefix).toBe("https://localhost:9000/api/0");
  });
});

describe("Content-Type validation", () => {
  it("should throw error when receiving HTML instead of JSON", async () => {
    const htmlContent = `<!DOCTYPE html>
<html>
<head><title>Login Required</title></head>
<body><h1>Please log in</h1></body>
</html>`;

    globalThis.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      statusText: "OK",
      headers: {
        get: (key: string) =>
          key === "content-type" ? "text/html; charset=utf-8" : null,
      },
      text: () => Promise.resolve(htmlContent),
    });

    const apiService = new SentryApiService({
      host: "sentry.io",
      accessToken: "test-token",
    });

    await expect(apiService.getAuthenticatedUser()).rejects.toThrow(
      "Expected JSON response but received HTML (200 OK). This may indicate you're not authenticated, the URL is incorrect, or there's a server issue.",
    );
  });

  it("should throw error when receiving non-JSON content type", async () => {
    globalThis.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      statusText: "OK",
      headers: {
        get: (key: string) => (key === "content-type" ? "text/plain" : null),
      },
      text: () => Promise.resolve("Error: Something went wrong"),
    });

    const apiService = new SentryApiService({
      host: "sentry.io",
      accessToken: "test-token",
    });

    await expect(apiService.getAuthenticatedUser()).rejects.toThrow(
      "Expected JSON response but received text/plain (200 OK)",
    );
  });

  it("should throw error when no content-type header is present", async () => {
    globalThis.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      statusText: "OK",
      headers: {
        get: () => null,
      },
      text: () => Promise.resolve("Some non-JSON response"),
    });

    const apiService = new SentryApiService({
      host: "sentry.io",
      accessToken: "test-token",
    });

    await expect(apiService.getAuthenticatedUser()).rejects.toThrow(
      "Expected JSON response but received unknown content type (200 OK)",
    );
  });

  it("should parse JSON successfully when content-type is application/json", async () => {
    const mockUser = {
      id: "123",
      name: "Test User",
      email: "[email protected]",
    };

    globalThis.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      statusText: "OK",
      headers: {
        get: (key: string) =>
          key === "content-type" ? "application/json; charset=utf-8" : null,
      },
      json: () => Promise.resolve(mockUser),
    });

    const apiService = new SentryApiService({
      host: "sentry.io",
      accessToken: "test-token",
    });

    const result = await apiService.getAuthenticatedUser();
    expect(result).toEqual(mockUser);
  });

  it("should detect HTML content even without content-type header", async () => {
    const htmlContent = "<!DOCTYPE html><html><body>Error page</body></html>";

    globalThis.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      statusText: "OK",
      headers: {
        get: () => null,
      },
      text: () => Promise.resolve(htmlContent),
    });

    const apiService = new SentryApiService({
      host: "sentry.io",
      accessToken: "test-token",
    });

    await expect(apiService.getAuthenticatedUser()).rejects.toThrow(
      "Expected JSON response but received HTML (200 OK). This may indicate you're not authenticated, the URL is incorrect, or there's a server issue.",
    );
  });

  it("should handle HTML response from regions endpoint", async () => {
    const htmlContent = `<!DOCTYPE html>
<html>
<head><title>Login Required</title></head>
<body><h1>Please log in</h1></body>
</html>`;

    globalThis.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      statusText: "OK",
      headers: {
        get: (key: string) =>
          key === "content-type" ? "text/html; charset=utf-8" : null,
      },
      text: () => Promise.resolve(htmlContent),
    });

    const apiService = new SentryApiService({
      host: "sentry.io",
      accessToken: "test-token",
    });

    await expect(apiService.listOrganizations()).rejects.toThrow(
      "Expected JSON response but received HTML (200 OK). This may indicate you're not authenticated, the URL is incorrect, or there's a server issue.",
    );
  });
});

describe("API query builders", () => {
  describe("buildDiscoverApiQuery", () => {
    it("should build correct query for errors dataset", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });

      // @ts-expect-error - accessing private method for testing
      const params = apiService.buildDiscoverApiQuery({
        query: "level:error",
        fields: ["title", "project", "count()"],
        limit: 50,
        projectId: "backend",
        statsPeriod: "24h",
        sort: "-count()",
      });

      expect(params.toString()).toMatchInlineSnapshot(
        `"per_page=50&query=level%3Aerror&dataset=errors&statsPeriod=24h&project=backend&sort=-count&field=title&field=project&field=count%28%29"`,
      );
    });

    it("should transform aggregate sort parameters correctly", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });

      // @ts-expect-error - accessing private method for testing
      const params = apiService.buildDiscoverApiQuery({
        query: "",
        fields: ["error.type", "count()", "count_unique(user)"],
        limit: 10,
        sort: "-count(span.duration)",
      });

      expect(params.get("sort")).toBe("-count_span_duration");
    });

    it("should handle empty aggregate functions in sort", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });

      // @ts-expect-error - accessing private method for testing
      const params = apiService.buildDiscoverApiQuery({
        query: "",
        fields: ["title", "count()"],
        limit: 10,
        sort: "-count()",
      });

      expect(params.get("sort")).toBe("-count");
    });

    it("should safely handle malformed sort parameters", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });

      // @ts-expect-error - accessing private method for testing
      const params = apiService.buildDiscoverApiQuery({
        query: "",
        fields: ["title"],
        limit: 10,
        sort: "-count(((",
      });

      // Should not crash and should return the original sort if malformed
      expect(params.get("sort")).toBe("-count(((");
    });
  });

  describe("buildEapApiQuery", () => {
    it("should build correct query for spans dataset with sampling", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });

      // @ts-expect-error - accessing private method for testing
      const params = apiService.buildEapApiQuery({
        query: "span.op:db",
        fields: ["span.op", "span.description", "span.duration"],
        limit: 20,
        projectId: "frontend",
        dataset: "spans",
        statsPeriod: "1h",
        sort: "-span.duration",
      });

      expect(params.toString()).toMatchInlineSnapshot(
        `"per_page=20&query=span.op%3Adb&dataset=spans&statsPeriod=1h&project=frontend&sampling=NORMAL&sort=-span.duration&field=span.op&field=span.description&field=span.duration"`,
      );
    });

    it("should build correct query for logs dataset without sampling", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });

      // @ts-expect-error - accessing private method for testing
      const params = apiService.buildEapApiQuery({
        query: "severity:error",
        fields: ["timestamp", "message", "severity"],
        limit: 30,
        dataset: "ourlogs",
        sort: "-timestamp",
      });

      expect(params.toString()).toMatchInlineSnapshot(
        `"per_page=30&query=severity%3Aerror&dataset=ourlogs&sort=-timestamp&field=timestamp&field=message&field=severity"`,
      );

      // Verify sampling is not added for logs
      expect(params.has("sampling")).toBe(false);
    });

    it("should transform complex aggregate sorts with dots", () => {
      const apiService = new SentryApiService({ host: "sentry.io" });

      // @ts-expect-error - accessing private method for testing
      const params = apiService.buildEapApiQuery({
        query: "",
        fields: ["span.op", "avg(span.self_time)"],
        limit: 10,
        dataset: "spans",
        sort: "-avg(span.self_time)",
      });

      expect(params.get("sort")).toBe("-avg_span_self_time");
    });
  });

  describe("searchEvents integration", () => {
    it("should route errors dataset to Discover API builder", async () => {
      const apiService = new SentryApiService({
        host: "sentry.io",
        accessToken: "test-token",
      });

      // Mock the API response
      globalThis.fetch = vi.fn().mockResolvedValue({
        ok: true,
        headers: {
          get: (key: string) =>
            key === "content-type" ? "application/json" : null,
        },
        json: () => Promise.resolve({ data: [] }),
      });

      await apiService.searchEvents({
        organizationSlug: "test-org",
        query: "level:error",
        fields: ["title", "count()"],
        dataset: "errors",
        sort: "-count()",
      });

      // Verify the URL contains correct parameters
      expect(globalThis.fetch).toHaveBeenCalledWith(
        expect.stringContaining("dataset=errors"),
        expect.any(Object),
      );
      expect(globalThis.fetch).toHaveBeenCalledWith(
        expect.stringContaining("sort=-count"),
        expect.any(Object),
      );
    });

    it("should route spans dataset to EAP API builder with sampling", async () => {
      const apiService = new SentryApiService({
        host: "sentry.io",
        accessToken: "test-token",
      });

      // Mock the API response
      globalThis.fetch = vi.fn().mockResolvedValue({
        ok: true,
        headers: {
          get: (key: string) =>
            key === "content-type" ? "application/json" : null,
        },
        json: () => Promise.resolve({ data: [] }),
      });

      await apiService.searchEvents({
        organizationSlug: "test-org",
        query: "span.op:http",
        fields: ["span.op", "span.duration"],
        dataset: "spans",
      });

      // Verify the URL contains correct parameters
      expect(globalThis.fetch).toHaveBeenCalledWith(
        expect.stringContaining("dataset=spans"),
        expect.any(Object),
      );
      expect(globalThis.fetch).toHaveBeenCalledWith(
        expect.stringContaining("sampling=NORMAL"),
        expect.any(Object),
      );
    });
  });

  describe("Web URL builders", () => {
    describe("buildDiscoverUrl", () => {
      it("should build correct URL for errors dataset on SaaS", () => {
        const apiService = new SentryApiService({ host: "sentry.io" });

        // @ts-expect-error - accessing private method for testing
        const url = apiService.buildDiscoverUrl({
          organizationSlug: "my-org",
          query: "level:error",
          projectId: "backend",
          fields: ["title", "project", "timestamp"],
          sort: "-timestamp",
        });

        expect(url).toMatchInlineSnapshot(
          `"https://my-org.sentry.io/explore/discover/homepage/?dataset=errors&queryDataset=error-events&query=level%3Aerror&project=backend&field=title&field=project&field=timestamp&sort=-timestamp&statsPeriod=24h&yAxis=count%28%29"`,
        );
      });

      it("should include aggregate mode and yAxis for aggregate queries", () => {
        const apiService = new SentryApiService({ host: "sentry.io" });

        // @ts-expect-error - accessing private method for testing
        const url = apiService.buildDiscoverUrl({
          organizationSlug: "my-org",
          query: "is:unresolved",
          fields: ["title", "count()"],
          sort: "-count()",
          aggregateFunctions: ["count()"],
          groupByFields: ["title"],
        });

        expect(url).toContain("mode=aggregate");
        expect(url).toContain("yAxis=count%28%29");
        expect(url).toContain("field=title");
        expect(url).toContain("field=count%28%29");
      });

      it("should build correct URL for self-hosted", () => {
        const apiService = new SentryApiService({ host: "sentry.example.com" });

        // @ts-expect-error - accessing private method for testing
        const url = apiService.buildDiscoverUrl({
          organizationSlug: "my-org",
          query: "level:error",
          fields: ["title", "project"],
        });

        expect(url).toMatchInlineSnapshot(
          `"https://sentry.example.com/organizations/my-org/explore/discover/homepage/?dataset=errors&queryDataset=error-events&query=level%3Aerror&field=title&field=project&sort=-timestamp&statsPeriod=24h&yAxis=count%28%29"`,
        );
      });
    });

    describe("buildEapUrl", () => {
      it("should build correct URL for spans dataset with aggregate fields", () => {
        const apiService = new SentryApiService({ host: "sentry.io" });

        // @ts-expect-error - accessing private method for testing
        const url = apiService.buildEapUrl({
          organizationSlug: "my-org",
          query: "is_transaction:True",
          dataset: "spans",
          projectId: "123456",
          fields: ["span.description", "count()"],
          sort: "-count()",
          aggregateFunctions: ["count()"],
          groupByFields: ["span.description"],
        });

        expect(url).toContain("https://my-org.sentry.io/explore/traces/");
        expect(url).toContain("mode=aggregate");
        expect(url).toContain(
          `aggregateField=%7B%22groupBy%22%3A%22span.description%22%7D`,
        );
        expect(url).toContain(
          `aggregateField=%7B%22yAxes%22%3A%5B%22count%28%29%22%5D%7D`,
        );
        expect(url).toContain("project=123456");
        expect(url).toContain("query=is_transaction%3ATrue");
        expect(url).toContain("statsPeriod=24h");
      });

      it("should not include empty groupBy in aggregateField", () => {
        const apiService = new SentryApiService({ host: "sentry.io" });

        // @ts-expect-error - accessing private method for testing
        const url = apiService.buildEapUrl({
          organizationSlug: "my-org",
          query: "span.op:db",
          dataset: "spans",
          fields: ["count()"],
          sort: "-count()",
          aggregateFunctions: ["count()"],
          groupByFields: [],
        });

        expect(url).toContain("mode=aggregate");
        expect(url).toContain(
          `aggregateField=%7B%22yAxes%22%3A%5B%22count%28%29%22%5D%7D`,
        );
        expect(url).not.toContain("groupBy");
      });

      it("should handle multiple groupBy fields", () => {
        const apiService = new SentryApiService({ host: "sentry.io" });

        // @ts-expect-error - accessing private method for testing
        const url = apiService.buildEapUrl({
          organizationSlug: "my-org",
          query: "",
          dataset: "spans",
          fields: ["span.op", "span.description", "count()"],
          sort: "-count()",
          aggregateFunctions: ["count()"],
          groupByFields: ["span.op", "span.description"],
        });

        expect(url).toContain(
          `aggregateField=%7B%22groupBy%22%3A%22span.op%22%7D`,
        );
        expect(url).toContain(
          `aggregateField=%7B%22groupBy%22%3A%22span.description%22%7D`,
        );
        expect(url).toContain(
          `aggregateField=%7B%22yAxes%22%3A%5B%22count%28%29%22%5D%7D`,
        );
      });

      it("should handle non-aggregate queries", () => {
        const apiService = new SentryApiService({ host: "sentry.io" });

        // @ts-expect-error - accessing private method for testing
        const url = apiService.buildEapUrl({
          organizationSlug: "my-org",
          query: "span.op:http",
          dataset: "spans",
          fields: ["span.op", "span.description", "span.duration"],
          sort: "-span.duration",
        });

        expect(url).not.toContain("mode=aggregate");
        expect(url).not.toContain("aggregateField");
        expect(url).toContain("field=span.op");
        expect(url).toContain("field=span.description");
        expect(url).toContain("field=span.duration");
        expect(url).toContain("sort=-span.duration");
      });

      it("should use correct path for logs dataset", () => {
        const apiService = new SentryApiService({ host: "sentry.io" });

        // @ts-expect-error - accessing private method for testing
        const url = apiService.buildEapUrl({
          organizationSlug: "my-org",
          query: "severity:error",
          dataset: "logs",
          fields: ["timestamp", "message"],
        });

        expect(url).toContain("/explore/logs/");
        expect(url).not.toContain("/explore/traces/");
      });

      it("should handle self-hosted URLs correctly", () => {
        const apiService = new SentryApiService({ host: "sentry.example.com" });

        // @ts-expect-error - accessing private method for testing
        const url = apiService.buildEapUrl({
          organizationSlug: "my-org",
          query: "",
          dataset: "spans",
          fields: ["span.op"],
        });

        expect(url).toMatchInlineSnapshot(
          `"https://sentry.example.com/organizations/my-org/explore/traces/?query=&field=span.op&statsPeriod=24h&table=span"`,
        );
      });
    });
  });
});

```
Page 9/11FirstPrevNextLast