#
tokens: 28310/50000 1/408 files (page 14/15)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 14 of 15. Use http://codebase.md/getsentry/sentry-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .claude
│   ├── agents
│   │   └── claude-optimizer.md
│   ├── commands
│   │   ├── gh-pr.md
│   │   └── gh-review.md
│   └── settings.json
├── .craft.yml
├── .cursor
│   └── mcp.json
├── .env.example
├── .github
│   └── workflows
│       ├── deploy.yml
│       ├── eval.yml
│       ├── merge-jobs.yml
│       ├── release.yml
│       ├── smoke-tests.yml
│       └── test.yml
├── .gitignore
├── .mcp.json
├── .vscode
│   ├── extensions.json
│   ├── mcp.json
│   └── settings.json
├── AGENTS.md
├── bin
│   └── bump-version.sh
├── biome.json
├── CLAUDE.md
├── codecov.yml
├── core
├── docs
│   ├── adding-tools.mdc
│   ├── api-patterns.mdc
│   ├── architecture.mdc
│   ├── cloudflare
│   │   ├── architecture.md
│   │   ├── deployment.md
│   │   ├── oauth-architecture.md
│   │   └── overview.md
│   ├── coding-guidelines.mdc
│   ├── common-patterns.mdc
│   ├── cursor.mdc
│   ├── deployment.mdc
│   ├── error-handling.mdc
│   ├── github-actions.mdc
│   ├── llms
│   │   ├── document-scopes.mdc
│   │   ├── documentation-style-guide.mdc
│   │   └── README.md
│   ├── logging.mdc
│   ├── monitoring.mdc
│   ├── permissions-and-scopes.md
│   ├── pr-management.mdc
│   ├── quality-checks.mdc
│   ├── README.md
│   ├── search-events-api-patterns.md
│   ├── security.mdc
│   ├── specs
│   │   ├── README.md
│   │   ├── search-events.md
│   │   └── subpath-constraints.md
│   └── testing.mdc
├── LICENSE.md
├── Makefile
├── package.json
├── packages
│   ├── mcp-cloudflare
│   │   ├── .env.example
│   │   ├── components.json
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── public
│   │   │   ├── favicon.ico
│   │   │   ├── flow-transparent.png
│   │   │   └── flow.jpg
│   │   ├── src
│   │   │   ├── client
│   │   │   │   ├── app.tsx
│   │   │   │   ├── components
│   │   │   │   │   ├── chat
│   │   │   │   │   │   ├── auth-form.tsx
│   │   │   │   │   │   ├── chat-input.tsx
│   │   │   │   │   │   ├── chat-message.tsx
│   │   │   │   │   │   ├── chat-messages.tsx
│   │   │   │   │   │   ├── chat-ui.tsx
│   │   │   │   │   │   ├── chat.tsx
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── tool-invocation.tsx
│   │   │   │   │   │   └── types.ts
│   │   │   │   │   ├── fragments
│   │   │   │   │   │   ├── remote-setup.tsx
│   │   │   │   │   │   ├── setup-guide.tsx
│   │   │   │   │   │   └── stdio-setup.tsx
│   │   │   │   │   └── ui
│   │   │   │   │       ├── accordion.tsx
│   │   │   │   │       ├── backdrop.tsx
│   │   │   │   │       ├── 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-handler.test.ts
│   │   │   │   │   ├── mcp-handler.ts
│   │   │   │   │   └── slug-validation.ts
│   │   │   │   ├── logging.ts
│   │   │   │   ├── oauth
│   │   │   │   │   ├── authorize.test.ts
│   │   │   │   │   ├── callback.test.ts
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── helpers.test.ts
│   │   │   │   │   ├── helpers.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── authorize.ts
│   │   │   │   │   │   ├── callback.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   └── state.ts
│   │   │   │   ├── routes
│   │   │   │   │   ├── chat-oauth.ts
│   │   │   │   │   ├── chat.ts
│   │   │   │   │   ├── mcp.ts
│   │   │   │   │   ├── metadata.ts
│   │   │   │   │   ├── search.test.ts
│   │   │   │   │   └── search.ts
│   │   │   │   ├── sentry.config.ts
│   │   │   │   ├── types
│   │   │   │   │   └── chat.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── utils
│   │   │   │       └── auth-errors.ts
│   │   │   └── test-setup.ts
│   │   ├── tsconfig.client.json
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   ├── tsconfig.server.json
│   │   ├── vite.config.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   ├── wrangler.canary.jsonc
│   │   └── wrangler.jsonc
│   ├── mcp-server
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── scripts
│   │   │   ├── generate-definitions.ts
│   │   │   └── generate-otel-namespaces.ts
│   │   ├── src
│   │   │   ├── api-client
│   │   │   │   ├── client.test.ts
│   │   │   │   ├── client.ts
│   │   │   │   ├── errors.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── schema.ts
│   │   │   │   └── types.ts
│   │   │   ├── cli
│   │   │   │   ├── parse.test.ts
│   │   │   │   ├── parse.ts
│   │   │   │   ├── resolve.test.ts
│   │   │   │   ├── resolve.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── usage.ts
│   │   │   ├── constants.ts
│   │   │   ├── errors.test.ts
│   │   │   ├── errors.ts
│   │   │   ├── index.ts
│   │   │   ├── internal
│   │   │   │   ├── agents
│   │   │   │   │   ├── callEmbeddedAgent.ts
│   │   │   │   │   ├── openai-provider.ts
│   │   │   │   │   └── tools
│   │   │   │   │       ├── data
│   │   │   │   │       │   ├── __namespaces.json
│   │   │   │   │       │   ├── android.json
│   │   │   │   │       │   ├── app.json
│   │   │   │   │       │   ├── artifact.json
│   │   │   │   │       │   ├── aspnetcore.json
│   │   │   │   │       │   ├── aws.json
│   │   │   │   │       │   ├── azure.json
│   │   │   │   │       │   ├── browser.json
│   │   │   │   │       │   ├── cassandra.json
│   │   │   │   │       │   ├── cicd.json
│   │   │   │   │       │   ├── CLAUDE.md
│   │   │   │   │       │   ├── client.json
│   │   │   │   │       │   ├── cloud.json
│   │   │   │   │       │   ├── cloudevents.json
│   │   │   │   │       │   ├── cloudfoundry.json
│   │   │   │   │       │   ├── code.json
│   │   │   │   │       │   ├── container.json
│   │   │   │   │       │   ├── cpu.json
│   │   │   │   │       │   ├── cpython.json
│   │   │   │   │       │   ├── database.json
│   │   │   │   │       │   ├── db.json
│   │   │   │   │       │   ├── deployment.json
│   │   │   │   │       │   ├── destination.json
│   │   │   │   │       │   ├── device.json
│   │   │   │   │       │   ├── disk.json
│   │   │   │   │       │   ├── dns.json
│   │   │   │   │       │   ├── dotnet.json
│   │   │   │   │       │   ├── elasticsearch.json
│   │   │   │   │       │   ├── enduser.json
│   │   │   │   │       │   ├── error.json
│   │   │   │   │       │   ├── faas.json
│   │   │   │   │       │   ├── feature_flags.json
│   │   │   │   │       │   ├── file.json
│   │   │   │   │       │   ├── gcp.json
│   │   │   │   │       │   ├── gen_ai.json
│   │   │   │   │       │   ├── geo.json
│   │   │   │   │       │   ├── go.json
│   │   │   │   │       │   ├── graphql.json
│   │   │   │   │       │   ├── hardware.json
│   │   │   │   │       │   ├── heroku.json
│   │   │   │   │       │   ├── host.json
│   │   │   │   │       │   ├── http.json
│   │   │   │   │       │   ├── ios.json
│   │   │   │   │       │   ├── jvm.json
│   │   │   │   │       │   ├── k8s.json
│   │   │   │   │       │   ├── linux.json
│   │   │   │   │       │   ├── log.json
│   │   │   │   │       │   ├── mcp.json
│   │   │   │   │       │   ├── messaging.json
│   │   │   │   │       │   ├── network.json
│   │   │   │   │       │   ├── nodejs.json
│   │   │   │   │       │   ├── oci.json
│   │   │   │   │       │   ├── opentracing.json
│   │   │   │   │       │   ├── os.json
│   │   │   │   │       │   ├── otel.json
│   │   │   │   │       │   ├── peer.json
│   │   │   │   │       │   ├── process.json
│   │   │   │   │       │   ├── profile.json
│   │   │   │   │       │   ├── rpc.json
│   │   │   │   │       │   ├── server.json
│   │   │   │   │       │   ├── service.json
│   │   │   │   │       │   ├── session.json
│   │   │   │   │       │   ├── signalr.json
│   │   │   │   │       │   ├── source.json
│   │   │   │   │       │   ├── system.json
│   │   │   │   │       │   ├── telemetry.json
│   │   │   │   │       │   ├── test.json
│   │   │   │   │       │   ├── thread.json
│   │   │   │   │       │   ├── tls.json
│   │   │   │   │       │   ├── url.json
│   │   │   │   │       │   ├── user.json
│   │   │   │   │       │   ├── v8js.json
│   │   │   │   │       │   ├── vcs.json
│   │   │   │   │       │   ├── webengine.json
│   │   │   │   │       │   └── zos.json
│   │   │   │   │       ├── dataset-fields.test.ts
│   │   │   │   │       ├── dataset-fields.ts
│   │   │   │   │       ├── otel-semantics.test.ts
│   │   │   │   │       ├── otel-semantics.ts
│   │   │   │   │       ├── utils.ts
│   │   │   │   │       ├── whoami.test.ts
│   │   │   │   │       └── whoami.ts
│   │   │   │   ├── constraint-helpers.test.ts
│   │   │   │   ├── constraint-helpers.ts
│   │   │   │   ├── context-storage.ts
│   │   │   │   ├── error-handling.ts
│   │   │   │   ├── fetch-utils.test.ts
│   │   │   │   ├── fetch-utils.ts
│   │   │   │   ├── formatting.test.ts
│   │   │   │   ├── formatting.ts
│   │   │   │   ├── issue-helpers.test.ts
│   │   │   │   ├── issue-helpers.ts
│   │   │   │   ├── test-fixtures.ts
│   │   │   │   └── tool-helpers
│   │   │   │       ├── api.test.ts
│   │   │   │       ├── api.ts
│   │   │   │       ├── define.ts
│   │   │   │       ├── enhance-error.ts
│   │   │   │       ├── formatting.ts
│   │   │   │       ├── issue.ts
│   │   │   │       ├── seer.test.ts
│   │   │   │       ├── seer.ts
│   │   │   │       ├── validate-region-url.test.ts
│   │   │   │       └── validate-region-url.ts
│   │   │   ├── permissions.parseScopes.test.ts
│   │   │   ├── permissions.ts
│   │   │   ├── schema.ts
│   │   │   ├── server-context.test.ts
│   │   │   ├── server.ts
│   │   │   ├── telem
│   │   │   │   ├── index.ts
│   │   │   │   ├── logging.ts
│   │   │   │   ├── sentry.test.ts
│   │   │   │   └── sentry.ts
│   │   │   ├── test-setup.ts
│   │   │   ├── test-utils
│   │   │   │   └── context.ts
│   │   │   ├── toolDefinitions.ts
│   │   │   ├── tools
│   │   │   │   ├── 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/api-client/client.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import {
   2 |   getIssueUrl as getIssueUrlUtil,
   3 |   getTraceUrl as getTraceUrlUtil,
   4 |   isSentryHost,
   5 | } from "../utils/url-utils";
   6 | import { logWarn } from "../telem/logging";
   7 | import {
   8 |   OrganizationListSchema,
   9 |   OrganizationSchema,
  10 |   ClientKeySchema,
  11 |   TeamListSchema,
  12 |   TeamSchema,
  13 |   ProjectListSchema,
  14 |   ProjectSchema,
  15 |   ReleaseListSchema,
  16 |   IssueListSchema,
  17 |   IssueSchema,
  18 |   EventSchema,
  19 |   EventAttachmentListSchema,
  20 |   ErrorsSearchResponseSchema,
  21 |   SpansSearchResponseSchema,
  22 |   TagListSchema,
  23 |   ApiErrorSchema,
  24 |   ClientKeyListSchema,
  25 |   AutofixRunSchema,
  26 |   AutofixRunStateSchema,
  27 |   TraceMetaSchema,
  28 |   TraceSchema,
  29 |   UserSchema,
  30 |   UserRegionsSchema,
  31 | } from "./schema";
  32 | import { ConfigurationError } from "../errors";
  33 | import { createApiError, ApiNotFoundError, ApiValidationError } from "./errors";
  34 | import type {
  35 |   AutofixRun,
  36 |   AutofixRunState,
  37 |   ClientKey,
  38 |   ClientKeyList,
  39 |   Event,
  40 |   EventAttachment,
  41 |   EventAttachmentList,
  42 |   Issue,
  43 |   IssueList,
  44 |   OrganizationList,
  45 |   Project,
  46 |   ProjectList,
  47 |   ReleaseList,
  48 |   TagList,
  49 |   Team,
  50 |   TeamList,
  51 |   Trace,
  52 |   TraceMeta,
  53 |   User,
  54 | } from "./types";
  55 | // TODO: this is shared - so ideally, for safety, it uses @sentry/core, but currently
  56 | // logger isnt exposed (or rather, it is, but its not the right logger)
  57 | // import { logger } from "@sentry/node";
  58 | 
  59 | /**
  60 |  * Mapping of common network error codes to user-friendly messages.
  61 |  * These help users understand and resolve connection issues.
  62 |  */
  63 | const NETWORK_ERROR_MESSAGES: Record<string, string> = {
  64 |   EAI_AGAIN: "DNS temporarily unavailable. Check your internet connection.",
  65 |   ENOTFOUND: "Hostname not found. Verify the URL is correct.",
  66 |   ECONNREFUSED: "Connection refused. Ensure the service is accessible.",
  67 |   ETIMEDOUT: "Connection timed out. Check network connectivity.",
  68 |   ECONNRESET: "Connection reset. Try again in a moment.",
  69 | };
  70 | 
  71 | /**
  72 |  * Custom error class for Sentry API responses.
  73 |  *
  74 |  * Provides enhanced error messages for LLM consumption and handles
  75 |  * common API error scenarios with user-friendly messaging.
  76 |  *
  77 |  * @example
  78 |  * ```typescript
  79 |  * try {
  80 |  *   await apiService.listIssues({ organizationSlug: "invalid" });
  81 |  * } catch (error) {
  82 |  *   if (error instanceof ApiError) {
  83 |  *     console.log(`API Error ${error.status}: ${error.message}`);
  84 |  *   }
  85 |  * }
  86 |  * ```
  87 |  */
  88 | 
  89 | type RequestOptions = {
  90 |   host?: string;
  91 | };
  92 | 
  93 | /**
  94 |  * Sentry API client service for interacting with Sentry's REST API.
  95 |  *
  96 |  * This service provides a comprehensive interface to Sentry's API endpoints,
  97 |  * handling authentication, error processing, multi-region support, and
  98 |  * response validation through Zod schemas.
  99 |  *
 100 |  * Key Features:
 101 |  * - Multi-region support for Sentry SaaS and self-hosted instances
 102 |  * - Automatic schema validation with Zod
 103 |  * - Enhanced error handling with LLM-friendly messages
 104 |  * - URL generation for Sentry resources (issues, traces)
 105 |  * - Bearer token authentication
 106 |  * - Always uses HTTPS for secure connections
 107 |  *
 108 |  * @example Basic Usage
 109 |  * ```typescript
 110 |  * const apiService = new SentryApiService({
 111 |  *   accessToken: "your-token",
 112 |  *   host: "sentry.io"
 113 |  * });
 114 |  *
 115 |  * const orgs = await apiService.listOrganizations();
 116 |  * const issues = await apiService.listIssues({
 117 |  *   organizationSlug: "my-org",
 118 |  *   query: "is:unresolved"
 119 |  * });
 120 |  * ```
 121 |  *
 122 |  * @example Multi-Region Support
 123 |  * ```typescript
 124 |  * // Self-hosted instance with hostname
 125 |  * const selfHosted = new SentryApiService({
 126 |  *   accessToken: "token",
 127 |  *   host: "sentry.company.com"
 128 |  * });
 129 |  *
 130 |  * // Regional endpoint override
 131 |  * const issues = await apiService.listIssues(
 132 |  *   { organizationSlug: "org" },
 133 |  *   { host: "eu.sentry.io" }
 134 |  * );
 135 |  * ```
 136 |  */
 137 | export class SentryApiService {
 138 |   private accessToken: string | null;
 139 |   protected host: string;
 140 |   protected apiPrefix: string;
 141 | 
 142 |   /**
 143 |    * Creates a new Sentry API service instance.
 144 |    *
 145 |    * Always uses HTTPS for secure connections.
 146 |    *
 147 |    * @param config Configuration object
 148 |    * @param config.accessToken OAuth access token for authentication (optional for some endpoints)
 149 |    * @param config.host Sentry hostname (e.g. "sentry.io", "sentry.example.com")
 150 |    */
 151 |   constructor({
 152 |     accessToken = null,
 153 |     host = "sentry.io",
 154 |   }: {
 155 |     accessToken?: string | null;
 156 |     host?: string;
 157 |   }) {
 158 |     this.accessToken = accessToken;
 159 |     this.host = host;
 160 |     this.apiPrefix = `https://${host}/api/0`;
 161 |   }
 162 | 
 163 |   /**
 164 |    * Updates the host for API requests.
 165 |    *
 166 |    * Used for multi-region support or switching between Sentry instances.
 167 |    * Always uses HTTPS protocol.
 168 |    *
 169 |    * @param host New hostname to use for API requests
 170 |    */
 171 |   setHost(host: string) {
 172 |     this.host = host;
 173 |     this.apiPrefix = `https://${this.host}/api/0`;
 174 |   }
 175 | 
 176 |   /**
 177 |    * Checks if the current host is Sentry SaaS (sentry.io).
 178 |    *
 179 |    * Used to determine API endpoint availability and URL formats.
 180 |    * Self-hosted instances may not have all endpoints available.
 181 |    *
 182 |    * @returns True if using Sentry SaaS, false for self-hosted instances
 183 |    */
 184 |   private isSaas(): boolean {
 185 |     return isSentryHost(this.host);
 186 |   }
 187 | 
 188 |   /**
 189 |    * Internal method for making authenticated requests to Sentry API.
 190 |    *
 191 |    * Handles:
 192 |    * - Bearer token authentication
 193 |    * - Error response parsing and enhancement
 194 |    * - Multi-region host overrides
 195 |    * - Fetch availability validation
 196 |    *
 197 |    * @param path API endpoint path (without /api/0 prefix)
 198 |    * @param options Fetch options
 199 |    * @param requestOptions Additional request configuration
 200 |    * @returns Promise resolving to Response object
 201 |    * @throws {ApiError} Enhanced API errors with user-friendly messages
 202 |    * @throws {Error} Network or parsing errors
 203 |    */
 204 |   private async request(
 205 |     path: string,
 206 |     options: RequestInit = {},
 207 |     { host }: { host?: string } = {},
 208 |   ): Promise<Response> {
 209 |     const url = host
 210 |       ? `https://${host}/api/0${path}`
 211 |       : `${this.apiPrefix}${path}`;
 212 | 
 213 |     const headers: Record<string, string> = {
 214 |       "Content-Type": "application/json",
 215 |       "User-Agent": "Sentry MCP Server",
 216 |     };
 217 |     if (this.accessToken) {
 218 |       headers.Authorization = `Bearer ${this.accessToken}`;
 219 |     }
 220 | 
 221 |     // Check if fetch is available, otherwise provide a helpful error message
 222 |     if (typeof globalThis.fetch === "undefined") {
 223 |       throw new ConfigurationError(
 224 |         "fetch is not available. Please use Node.js >= 18 or ensure fetch is available in your environment.",
 225 |       );
 226 |     }
 227 | 
 228 |     // logger.info(logger.fmt`[sentryApi] ${options.method || "GET"} ${url}`);
 229 |     let response: Response;
 230 |     try {
 231 |       response = await fetch(url, {
 232 |         ...options,
 233 |         headers,
 234 |       });
 235 |     } catch (error) {
 236 |       // Extract the root cause from the error chain
 237 |       let rootCause = error;
 238 |       while (rootCause instanceof Error && rootCause.cause) {
 239 |         rootCause = rootCause.cause;
 240 |       }
 241 | 
 242 |       const errorMessage =
 243 |         rootCause instanceof Error ? rootCause.message : String(rootCause);
 244 | 
 245 |       let friendlyMessage = `Unable to connect to ${url}`;
 246 | 
 247 |       // Check if we have a specific message for this error
 248 |       const errorCode = Object.keys(NETWORK_ERROR_MESSAGES).find((code) =>
 249 |         errorMessage.includes(code),
 250 |       );
 251 | 
 252 |       if (errorCode) {
 253 |         friendlyMessage += ` - ${NETWORK_ERROR_MESSAGES[errorCode]}`;
 254 |       } else {
 255 |         friendlyMessage += ` - ${errorMessage}`;
 256 |       }
 257 | 
 258 |       // DNS resolution failures and connection timeouts to custom hosts are configuration issues
 259 |       if (
 260 |         errorCode === "ENOTFOUND" ||
 261 |         errorCode === "EAI_AGAIN" ||
 262 |         errorCode === "ECONNREFUSED" ||
 263 |         errorCode === "ETIMEDOUT" ||
 264 |         errorMessage.includes("Connect Timeout Error")
 265 |       ) {
 266 |         throw new ConfigurationError(friendlyMessage, { cause: error });
 267 |       }
 268 | 
 269 |       throw new Error(friendlyMessage, { cause: error });
 270 |     }
 271 | 
 272 |     // Handle error responses generically
 273 |     if (!response.ok) {
 274 |       const errorText = await response.text();
 275 |       let parsed: unknown | undefined;
 276 |       try {
 277 |         parsed = JSON.parse(errorText);
 278 |       } catch (error) {
 279 |         // If we can't parse JSON, check if it's HTML (server error)
 280 |         if (errorText.includes("<!DOCTYPE") || errorText.includes("<html")) {
 281 |           logWarn("Received HTML error page instead of JSON", {
 282 |             loggerScope: ["api", "client"],
 283 |             extra: {
 284 |               status: response.status,
 285 |               statusText: response.statusText,
 286 |               host: this.host,
 287 |               path,
 288 |               parseErrorMessage:
 289 |                 error instanceof Error ? error.message : String(error),
 290 |             },
 291 |           });
 292 |           // HTML response instead of JSON typically indicates a server configuration issue
 293 |           throw createApiError(
 294 |             `Server error: Received HTML instead of JSON (${response.status} ${response.statusText}). This may indicate an invalid URL or server issue.`,
 295 |             response.status,
 296 |             errorText,
 297 |             undefined,
 298 |           );
 299 |         }
 300 |         logWarn("Failed to parse JSON error response", {
 301 |           loggerScope: ["api", "client"],
 302 |           extra: {
 303 |             status: response.status,
 304 |             statusText: response.statusText,
 305 |             host: this.host,
 306 |             path,
 307 |             bodyPreview:
 308 |               errorText.length > 256
 309 |                 ? `${errorText.slice(0, 253)}…`
 310 |                 : errorText,
 311 |             parseErrorMessage:
 312 |               error instanceof Error ? error.message : String(error),
 313 |           },
 314 |         });
 315 |       }
 316 | 
 317 |       if (parsed) {
 318 |         const { data, success, error } = ApiErrorSchema.safeParse(parsed);
 319 | 
 320 |         if (success) {
 321 |           // Use the new error factory to create the appropriate error type
 322 |           throw createApiError(
 323 |             data.detail,
 324 |             response.status,
 325 |             data.detail,
 326 |             parsed,
 327 |           );
 328 |         }
 329 | 
 330 |         logWarn("Failed to parse validated API error response", {
 331 |           loggerScope: ["api", "client"],
 332 |           extra: {
 333 |             status: response.status,
 334 |             statusText: response.statusText,
 335 |             host: this.host,
 336 |             path,
 337 |             bodyPreview:
 338 |               errorText.length > 256
 339 |                 ? `${errorText.slice(0, 253)}…`
 340 |                 : errorText,
 341 |             validationErrorMessage:
 342 |               error instanceof Error ? error.message : String(error),
 343 |           },
 344 |         });
 345 |       }
 346 | 
 347 |       // Use the error factory to create the appropriate error type based on status
 348 |       throw createApiError(
 349 |         `API request failed: ${response.statusText}\n${errorText}`,
 350 |         response.status,
 351 |         errorText,
 352 |         undefined,
 353 |       );
 354 |     }
 355 | 
 356 |     return response;
 357 |   }
 358 | 
 359 |   /**
 360 |    * Safely parses a JSON response, checking Content-Type header first.
 361 |    *
 362 |    * @param response The Response object from fetch
 363 |    * @returns Promise resolving to the parsed JSON object
 364 |    * @throws {Error} If response is not JSON or parsing fails
 365 |    */
 366 |   private async parseJsonResponse(response: Response): Promise<unknown> {
 367 |     // Handle case where response might not have all properties (e.g., in tests or promise chains)
 368 |     if (!response.headers?.get) {
 369 |       return response.json();
 370 |     }
 371 | 
 372 |     const contentType = response.headers.get("content-type");
 373 | 
 374 |     // Check if the response is JSON
 375 |     if (!contentType || !contentType.includes("application/json")) {
 376 |       const responseText = await response.text();
 377 | 
 378 |       // Check if it's HTML
 379 |       if (
 380 |         contentType?.includes("text/html") ||
 381 |         responseText.includes("<!DOCTYPE") ||
 382 |         responseText.includes("<html")
 383 |       ) {
 384 |         // HTML when expecting JSON usually indicates authentication or routing issues
 385 |         throw new Error(
 386 |           `Expected JSON response but received HTML (${response.status} ${response.statusText}). This may indicate you're not authenticated, the URL is incorrect, or there's a server issue.`,
 387 |         );
 388 |       }
 389 | 
 390 |       // Generic non-JSON error
 391 |       throw new Error(
 392 |         `Expected JSON response but received ${contentType || "unknown content type"} ` +
 393 |           `(${response.status} ${response.statusText})`,
 394 |       );
 395 |     }
 396 | 
 397 |     try {
 398 |       return await response.json();
 399 |     } catch (error) {
 400 |       // JSON parsing failure after successful response
 401 |       throw new Error(
 402 |         `Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`,
 403 |       );
 404 |     }
 405 |   }
 406 | 
 407 |   /**
 408 |    * Makes a request to the Sentry API and parses the JSON response.
 409 |    *
 410 |    * This is the primary method for API calls that expect JSON responses.
 411 |    * It automatically validates Content-Type and provides helpful error messages
 412 |    * for common issues like authentication failures or server errors.
 413 |    *
 414 |    * @param path API endpoint path (without /api/0 prefix)
 415 |    * @param options Fetch options
 416 |    * @param requestOptions Additional request configuration
 417 |    * @returns Promise resolving to the parsed JSON response
 418 |    * @throws {ApiError} Enhanced API errors with user-friendly messages
 419 |    * @throws {Error} Network, parsing, or validation errors
 420 |    */
 421 |   private async requestJSON(
 422 |     path: string,
 423 |     options: RequestInit = {},
 424 |     requestOptions?: { host?: string },
 425 |   ): Promise<unknown> {
 426 |     const response = await this.request(path, options, requestOptions);
 427 |     return this.parseJsonResponse(response);
 428 |   }
 429 | 
 430 |   /**
 431 |    * Generates a Sentry issue URL for browser navigation.
 432 |    *
 433 |    * Handles both SaaS (subdomain-based) and self-hosted URL formats.
 434 |    * Always uses HTTPS protocol.
 435 |    *
 436 |    * @param organizationSlug Organization identifier
 437 |    * @param issueId Issue identifier (short ID or numeric ID)
 438 |    * @returns Full URL to the issue in Sentry UI
 439 |    *
 440 |    * @example
 441 |    * ```typescript
 442 |    * // SaaS: https://my-org.sentry.io/issues/PROJ-123
 443 |    * apiService.getIssueUrl("my-org", "PROJ-123")
 444 |    *
 445 |    * // Self-hosted: https://sentry.company.com/organizations/my-org/issues/PROJ-123
 446 |    * apiService.getIssueUrl("my-org", "PROJ-123")
 447 |    * ```
 448 |    */
 449 |   getIssueUrl(organizationSlug: string, issueId: string): string {
 450 |     return getIssueUrlUtil(this.host, organizationSlug, issueId);
 451 |   }
 452 | 
 453 |   /**
 454 |    * Generates a Sentry trace URL for performance investigation.
 455 |    *
 456 |    * Always uses HTTPS protocol.
 457 |    *
 458 |    * @param organizationSlug Organization identifier
 459 |    * @param traceId Trace identifier (hex string)
 460 |    * @returns Full HTTPS URL to the trace in Sentry UI
 461 |    *
 462 |    * @example
 463 |    * ```typescript
 464 |    * const traceUrl = apiService.getTraceUrl("my-org", "6a477f5b0f31ef7b6b9b5e1dea66c91d");
 465 |    * // https://my-org.sentry.io/explore/traces/trace/6a477f5b0f31ef7b6b9b5e1dea66c91d
 466 |    * ```
 467 |    */
 468 |   getTraceUrl(organizationSlug: string, traceId: string): string {
 469 |     return getTraceUrlUtil(this.host, organizationSlug, traceId);
 470 |   }
 471 | 
 472 |   // ================================================================================
 473 |   // URL BUILDERS FOR DIFFERENT SENTRY APIS
 474 |   // ================================================================================
 475 | 
 476 |   /**
 477 |    * Builds a URL for the legacy Discover API (used by errors dataset).
 478 |    *
 479 |    * The Discover API is the older query interface that includes aggregate
 480 |    * functions directly in the field list.
 481 |    *
 482 |    * @example
 483 |    * // URL format: /explore/discover/homepage/?field=title&field=count_unique(user)
 484 |    * buildDiscoverUrl("my-org", "level:error", "123", ["title", "count_unique(user)"], "-timestamp")
 485 |    */
 486 |   private buildDiscoverUrl(params: {
 487 |     organizationSlug: string;
 488 |     query: string;
 489 |     projectId?: string;
 490 |     fields?: string[];
 491 |     sort?: string;
 492 |     statsPeriod?: string;
 493 |     start?: string;
 494 |     end?: string;
 495 |     aggregateFunctions?: string[];
 496 |     groupByFields?: string[];
 497 |   }): string {
 498 |     const {
 499 |       organizationSlug,
 500 |       query,
 501 |       projectId,
 502 |       fields,
 503 |       sort,
 504 |       statsPeriod,
 505 |       start,
 506 |       end,
 507 |       aggregateFunctions,
 508 |       groupByFields,
 509 |     } = params;
 510 | 
 511 |     const urlParams = new URLSearchParams();
 512 | 
 513 |     // Discover API specific parameters
 514 |     urlParams.set("dataset", "errors");
 515 |     urlParams.set("queryDataset", "error-events");
 516 |     urlParams.set("query", query);
 517 | 
 518 |     if (projectId) {
 519 |       urlParams.set("project", projectId);
 520 |     }
 521 | 
 522 |     // Discover API includes aggregate functions directly in field list
 523 |     if (fields && fields.length > 0) {
 524 |       for (const field of fields) {
 525 |         urlParams.append("field", field);
 526 |       }
 527 |     } else {
 528 |       // Default fields for Discover
 529 |       urlParams.append("field", "title");
 530 |       urlParams.append("field", "project");
 531 |       urlParams.append("field", "user.display");
 532 |       urlParams.append("field", "timestamp");
 533 |     }
 534 | 
 535 |     urlParams.set("sort", sort || "-timestamp");
 536 | 
 537 |     // Add time parameters - either statsPeriod or start/end
 538 |     if (start && end) {
 539 |       urlParams.set("start", start);
 540 |       urlParams.set("end", end);
 541 |     } else {
 542 |       urlParams.set("statsPeriod", statsPeriod || "24h");
 543 |     }
 544 | 
 545 |     // Check if this is an aggregate query
 546 |     const isAggregate = (aggregateFunctions?.length ?? 0) > 0;
 547 |     if (isAggregate) {
 548 |       urlParams.set("mode", "aggregate");
 549 |       // For aggregate queries in Discover, set yAxis to the first aggregate function
 550 |       if (aggregateFunctions && aggregateFunctions.length > 0) {
 551 |         urlParams.set("yAxis", aggregateFunctions[0]);
 552 |       }
 553 |     } else {
 554 |       urlParams.set("yAxis", "count()");
 555 |     }
 556 | 
 557 |     // For SaaS instances, always use sentry.io for web UI URLs regardless of region
 558 |     // Regional subdomains (e.g., us.sentry.io) are only for API endpoints
 559 |     const webHost = this.isSaas() ? "sentry.io" : this.host;
 560 |     const path = this.isSaas()
 561 |       ? `https://${organizationSlug}.${webHost}/explore/discover/homepage/`
 562 |       : `https://${this.host}/organizations/${organizationSlug}/explore/discover/homepage/`;
 563 | 
 564 |     return `${path}?${urlParams.toString()}`;
 565 |   }
 566 | 
 567 |   /**
 568 |    * Builds a URL for the modern EAP (Event Analytics Platform) API used by spans/logs.
 569 |    *
 570 |    * The EAP API uses structured aggregate queries with separate aggregateField
 571 |    * parameters containing JSON objects for groupBy and yAxes.
 572 |    *
 573 |    * @example
 574 |    * // URL format: /explore/traces/?aggregateField={"groupBy":"span.op"}&aggregateField={"yAxes":["count()"]}
 575 |    * buildEapUrl("my-org", "span.op:db", "123", ["span.op", "count()"], "-count()", ["count()"], ["span.op"])
 576 |    */
 577 |   private buildEapUrl(params: {
 578 |     organizationSlug: string;
 579 |     query: string;
 580 |     dataset: "spans" | "logs";
 581 |     projectId?: string;
 582 |     fields?: string[];
 583 |     sort?: string;
 584 |     statsPeriod?: string;
 585 |     start?: string;
 586 |     end?: string;
 587 |     aggregateFunctions?: string[];
 588 |     groupByFields?: string[];
 589 |   }): string {
 590 |     const {
 591 |       organizationSlug,
 592 |       query,
 593 |       dataset,
 594 |       projectId,
 595 |       fields,
 596 |       sort,
 597 |       statsPeriod,
 598 |       start,
 599 |       end,
 600 |       aggregateFunctions,
 601 |       groupByFields,
 602 |     } = params;
 603 | 
 604 |     const urlParams = new URLSearchParams();
 605 |     urlParams.set("query", query);
 606 | 
 607 |     if (projectId) {
 608 |       urlParams.set("project", projectId);
 609 |     }
 610 | 
 611 |     // Determine if this is an aggregate query
 612 |     const isAggregateQuery =
 613 |       (aggregateFunctions?.length ?? 0) > 0 ||
 614 |       fields?.some((field) => field.includes("(") && field.includes(")")) ||
 615 |       false;
 616 | 
 617 |     if (isAggregateQuery) {
 618 |       // EAP API uses structured aggregate parameters
 619 |       if (
 620 |         (aggregateFunctions?.length ?? 0) > 0 ||
 621 |         (groupByFields?.length ?? 0) > 0
 622 |       ) {
 623 |         // Add each groupBy field as a separate aggregateField parameter
 624 |         if (groupByFields && groupByFields.length > 0) {
 625 |           for (const field of groupByFields) {
 626 |             urlParams.append(
 627 |               "aggregateField",
 628 |               JSON.stringify({ groupBy: field }),
 629 |             );
 630 |           }
 631 |         }
 632 | 
 633 |         // Add aggregate functions (yAxes)
 634 |         if (aggregateFunctions && aggregateFunctions.length > 0) {
 635 |           urlParams.append(
 636 |             "aggregateField",
 637 |             JSON.stringify({ yAxes: aggregateFunctions }),
 638 |           );
 639 |         }
 640 |       } else {
 641 |         // Fallback: parse fields to extract aggregate info
 642 |         const parsedGroupByFields =
 643 |           fields?.filter(
 644 |             (field) => !field.includes("(") && !field.includes(")"),
 645 |           ) || [];
 646 |         const parsedAggregateFunctions =
 647 |           fields?.filter(
 648 |             (field) => field.includes("(") && field.includes(")"),
 649 |           ) || [];
 650 | 
 651 |         for (const field of parsedGroupByFields) {
 652 |           urlParams.append(
 653 |             "aggregateField",
 654 |             JSON.stringify({ groupBy: field }),
 655 |           );
 656 |         }
 657 | 
 658 |         if (parsedAggregateFunctions.length > 0) {
 659 |           urlParams.append(
 660 |             "aggregateField",
 661 |             JSON.stringify({ yAxes: parsedAggregateFunctions }),
 662 |           );
 663 |         }
 664 |       }
 665 | 
 666 |       urlParams.set("mode", "aggregate");
 667 |     } else {
 668 |       // Non-aggregate query, add individual fields
 669 |       if (fields && fields.length > 0) {
 670 |         for (const field of fields) {
 671 |           urlParams.append("field", field);
 672 |         }
 673 |       }
 674 |     }
 675 | 
 676 |     // Add sort parameter for all queries
 677 |     if (sort) {
 678 |       urlParams.set("sort", sort);
 679 |     }
 680 | 
 681 |     // Add time parameters - either statsPeriod or start/end
 682 |     if (start && end) {
 683 |       urlParams.set("start", start);
 684 |       urlParams.set("end", end);
 685 |     } else {
 686 |       urlParams.set("statsPeriod", statsPeriod || "24h");
 687 |     }
 688 | 
 689 |     // Add table parameter for spans dataset (required for UI)
 690 |     if (dataset === "spans") {
 691 |       urlParams.set("table", "span");
 692 |     }
 693 | 
 694 |     const basePath = dataset === "logs" ? "logs" : "traces";
 695 |     // For SaaS instances, always use sentry.io for web UI URLs regardless of region
 696 |     // Regional subdomains (e.g., us.sentry.io) are only for API endpoints
 697 |     const webHost = this.isSaas() ? "sentry.io" : this.host;
 698 |     const path = this.isSaas()
 699 |       ? `https://${organizationSlug}.${webHost}/explore/${basePath}/`
 700 |       : `https://${this.host}/organizations/${organizationSlug}/explore/${basePath}/`;
 701 | 
 702 |     return `${path}?${urlParams.toString()}`;
 703 |   }
 704 | 
 705 |   /**
 706 |    * Generates a Sentry events explorer URL for viewing search results.
 707 |    *
 708 |    * Routes to the appropriate API based on dataset:
 709 |    * - Errors: Uses legacy Discover API
 710 |    * - Spans/Logs: Uses modern EAP (Event Analytics Platform) API
 711 |    *
 712 |    * @param organizationSlug Organization identifier
 713 |    * @param query Sentry search query
 714 |    * @param projectId Optional project filter
 715 |    * @param dataset Dataset type (spans, errors, or logs)
 716 |    * @param fields Array of fields to include in results
 717 |    * @param sort Sort parameter (e.g., "-timestamp", "-count()")
 718 |    * @param aggregateFunctions Array of aggregate functions (only used for EAP datasets)
 719 |    * @param groupByFields Array of fields to group by (only used for EAP datasets)
 720 |    * @param statsPeriod Relative time period (e.g., "24h", "7d")
 721 |    * @param start Absolute start time (ISO 8601)
 722 |    * @param end Absolute end time (ISO 8601)
 723 |    * @returns Full HTTPS URL to the events explorer in Sentry UI
 724 |    */
 725 |   getEventsExplorerUrl(
 726 |     organizationSlug: string,
 727 |     query: string,
 728 |     projectId?: string,
 729 |     dataset: "spans" | "errors" | "logs" = "spans",
 730 |     fields?: string[],
 731 |     sort?: string,
 732 |     aggregateFunctions?: string[],
 733 |     groupByFields?: string[],
 734 |     statsPeriod?: string,
 735 |     start?: string,
 736 |     end?: string,
 737 |   ): string {
 738 |     if (dataset === "errors") {
 739 |       // Route to legacy Discover API
 740 |       return this.buildDiscoverUrl({
 741 |         organizationSlug,
 742 |         query,
 743 |         projectId,
 744 |         fields,
 745 |         sort,
 746 |         statsPeriod,
 747 |         start,
 748 |         end,
 749 |         aggregateFunctions,
 750 |         groupByFields,
 751 |       });
 752 |     }
 753 | 
 754 |     // Route to modern EAP API (spans and logs)
 755 |     return this.buildEapUrl({
 756 |       organizationSlug,
 757 |       query,
 758 |       dataset,
 759 |       projectId,
 760 |       fields,
 761 |       sort,
 762 |       statsPeriod,
 763 |       start,
 764 |       end,
 765 |       aggregateFunctions,
 766 |       groupByFields,
 767 |     });
 768 |   }
 769 | 
 770 |   /**
 771 |    * Retrieves the authenticated user's profile information.
 772 |    *
 773 |    * @param opts Request options including host override
 774 |    * @returns User profile data
 775 |    * @throws {ApiError} If authentication fails or user not found
 776 |    */
 777 |   async getAuthenticatedUser(opts?: RequestOptions): Promise<User> {
 778 |     // Auth endpoints only exist on the main API server, never on regional endpoints
 779 |     let authHost: string | undefined;
 780 | 
 781 |     if (this.isSaas()) {
 782 |       // For SaaS, always use the main sentry.io host, not regional hosts
 783 |       // This handles cases like us.sentry.io, eu.sentry.io, etc.
 784 |       authHost = "sentry.io";
 785 |     }
 786 |     // For self-hosted, use the configured host (authHost remains undefined)
 787 | 
 788 |     const body = await this.requestJSON("/auth/", undefined, {
 789 |       ...opts,
 790 |       host: authHost,
 791 |     });
 792 |     return UserSchema.parse(body);
 793 |   }
 794 | 
 795 |   /**
 796 |    * Lists all organizations accessible to the authenticated user.
 797 |    *
 798 |    * Automatically handles multi-region queries by fetching from all
 799 |    * available regions and combining results.
 800 |    *
 801 |    * @param params Query parameters
 802 |    * @param params.query Search query to filter organizations by name/slug
 803 |    * @param opts Request options
 804 |    * @returns Array of organizations across all accessible regions (limited to 25 results)
 805 |    *
 806 |    * @example
 807 |    * ```typescript
 808 |    * const orgs = await apiService.listOrganizations();
 809 |    * orgs.forEach(org => {
 810 |    *   // regionUrl present for Cloud Service, empty for self-hosted
 811 |    *   console.log(`${org.name} (${org.slug}) - ${org.links?.regionUrl || 'No region URL'}`);
 812 |    * });
 813 |    * ```
 814 |    */
 815 |   async listOrganizations(
 816 |     params?: { query?: string },
 817 |     opts?: RequestOptions,
 818 |   ): Promise<OrganizationList> {
 819 |     // Build query parameters
 820 |     const queryParams = new URLSearchParams();
 821 |     queryParams.set("per_page", "25");
 822 |     if (params?.query) {
 823 |       queryParams.set("query", params.query);
 824 |     }
 825 |     const queryString = queryParams.toString();
 826 |     const path = `/organizations/?${queryString}`;
 827 | 
 828 |     // For self-hosted instances, the regions endpoint doesn't exist
 829 |     if (!this.isSaas()) {
 830 |       const body = await this.requestJSON(path, undefined, opts);
 831 |       return OrganizationListSchema.parse(body);
 832 |     }
 833 | 
 834 |     // For SaaS, try to use regions endpoint first
 835 |     try {
 836 |       // TODO: Sentry is currently not returning all orgs without hitting region endpoints
 837 |       // The regions endpoint only exists on the main API server, not on regional endpoints
 838 |       const regionsBody = await this.requestJSON(
 839 |         "/users/me/regions/",
 840 |         undefined,
 841 |         {}, // Don't pass opts to ensure we use the main host
 842 |       );
 843 |       const regionData = UserRegionsSchema.parse(regionsBody);
 844 | 
 845 |       const allOrganizations = (
 846 |         await Promise.all(
 847 |           regionData.regions.map(async (region) =>
 848 |             this.requestJSON(path, undefined, {
 849 |               ...opts,
 850 |               host: new URL(region.url).host,
 851 |             }),
 852 |           ),
 853 |         )
 854 |       )
 855 |         .map((data) => OrganizationListSchema.parse(data))
 856 |         .reduce((acc, curr) => acc.concat(curr), []);
 857 | 
 858 |       // Apply the limit after combining results from all regions
 859 |       return allOrganizations.slice(0, 25);
 860 |     } catch (error) {
 861 |       // If regions endpoint fails (e.g., older self-hosted versions identifying as sentry.io),
 862 |       // fall back to direct organizations endpoint
 863 |       if (error instanceof ApiNotFoundError) {
 864 |         // logger.info("Regions endpoint not found, falling back to direct organizations endpoint");
 865 |         const body = await this.requestJSON(path, undefined, opts);
 866 |         return OrganizationListSchema.parse(body);
 867 |       }
 868 | 
 869 |       // Re-throw other errors
 870 |       throw error;
 871 |     }
 872 |   }
 873 | 
 874 |   /**
 875 |    * Gets a single organization by slug.
 876 |    *
 877 |    * @param organizationSlug Organization identifier
 878 |    * @param opts Request options including host override
 879 |    * @returns Organization data
 880 |    */
 881 |   async getOrganization(organizationSlug: string, opts?: RequestOptions) {
 882 |     const body = await this.requestJSON(
 883 |       `/organizations/${organizationSlug}/`,
 884 |       undefined,
 885 |       opts,
 886 |     );
 887 |     return OrganizationSchema.parse(body);
 888 |   }
 889 | 
 890 |   /**
 891 |    * Lists teams within an organization.
 892 |    *
 893 |    * @param organizationSlug Organization identifier
 894 |    * @param params Query parameters
 895 |    * @param params.query Search query to filter teams by name/slug
 896 |    * @param opts Request options including host override
 897 |    * @returns Array of teams in the organization (limited to 25 results)
 898 |    */
 899 |   async listTeams(
 900 |     organizationSlug: string,
 901 |     params?: { query?: string },
 902 |     opts?: RequestOptions,
 903 |   ): Promise<TeamList> {
 904 |     const queryParams = new URLSearchParams();
 905 |     queryParams.set("per_page", "25");
 906 |     if (params?.query) {
 907 |       queryParams.set("query", params.query);
 908 |     }
 909 |     const queryString = queryParams.toString();
 910 |     const path = `/organizations/${organizationSlug}/teams/?${queryString}`;
 911 | 
 912 |     const body = await this.requestJSON(path, undefined, opts);
 913 |     return TeamListSchema.parse(body);
 914 |   }
 915 | 
 916 |   /**
 917 |    * Creates a new team within an organization.
 918 |    *
 919 |    * @param params Team creation parameters
 920 |    * @param params.organizationSlug Organization identifier
 921 |    * @param params.name Team name
 922 |    * @param opts Request options
 923 |    * @returns Created team data
 924 |    * @throws {ApiError} If team creation fails (e.g., name conflicts)
 925 |    */
 926 |   async createTeam(
 927 |     {
 928 |       organizationSlug,
 929 |       name,
 930 |     }: {
 931 |       organizationSlug: string;
 932 |       name: string;
 933 |     },
 934 |     opts?: RequestOptions,
 935 |   ): Promise<Team> {
 936 |     const body = await this.requestJSON(
 937 |       `/organizations/${organizationSlug}/teams/`,
 938 |       {
 939 |         method: "POST",
 940 |         body: JSON.stringify({ name }),
 941 |       },
 942 |       opts,
 943 |     );
 944 |     return TeamSchema.parse(body);
 945 |   }
 946 | 
 947 |   /**
 948 |    * Lists projects within an organization.
 949 |    *
 950 |    * @param organizationSlug Organization identifier
 951 |    * @param params Query parameters
 952 |    * @param params.query Search query to filter projects by name/slug
 953 |    * @param opts Request options
 954 |    * @returns Array of projects in the organization (limited to 25 results)
 955 |    */
 956 |   async listProjects(
 957 |     organizationSlug: string,
 958 |     params?: { query?: string },
 959 |     opts?: RequestOptions,
 960 |   ): Promise<ProjectList> {
 961 |     const queryParams = new URLSearchParams();
 962 |     queryParams.set("per_page", "25");
 963 |     if (params?.query) {
 964 |       queryParams.set("query", params.query);
 965 |     }
 966 |     const queryString = queryParams.toString();
 967 |     const path = `/organizations/${organizationSlug}/projects/?${queryString}`;
 968 | 
 969 |     const body = await this.requestJSON(path, undefined, opts);
 970 |     return ProjectListSchema.parse(body);
 971 |   }
 972 | 
 973 |   /**
 974 |    * Gets a single project by slug or ID.
 975 |    *
 976 |    * @param params Project fetch parameters
 977 |    * @param params.organizationSlug Organization identifier
 978 |    * @param params.projectSlugOrId Project slug or numeric ID
 979 |    * @param opts Request options
 980 |    * @returns Project data
 981 |    */
 982 |   async getProject(
 983 |     {
 984 |       organizationSlug,
 985 |       projectSlugOrId,
 986 |     }: {
 987 |       organizationSlug: string;
 988 |       projectSlugOrId: string;
 989 |     },
 990 |     opts?: RequestOptions,
 991 |   ): Promise<Project> {
 992 |     const body = await this.requestJSON(
 993 |       `/projects/${organizationSlug}/${projectSlugOrId}/`,
 994 |       undefined,
 995 |       opts,
 996 |     );
 997 |     return ProjectSchema.parse(body);
 998 |   }
 999 | 
1000 |   /**
1001 |    * Creates a new project within a team.
1002 |    *
1003 |    * @param params Project creation parameters
1004 |    * @param params.organizationSlug Organization identifier
1005 |    * @param params.teamSlug Team identifier
1006 |    * @param params.name Project name
1007 |    * @param params.platform Platform identifier (e.g., "javascript", "python")
1008 |    * @param opts Request options
1009 |    * @returns Created project data
1010 |    */
1011 |   async createProject(
1012 |     {
1013 |       organizationSlug,
1014 |       teamSlug,
1015 |       name,
1016 |       platform,
1017 |     }: {
1018 |       organizationSlug: string;
1019 |       teamSlug: string;
1020 |       name: string;
1021 |       platform?: string;
1022 |     },
1023 |     opts?: RequestOptions,
1024 |   ): Promise<Project> {
1025 |     const body = await this.requestJSON(
1026 |       `/teams/${organizationSlug}/${teamSlug}/projects/`,
1027 |       {
1028 |         method: "POST",
1029 |         body: JSON.stringify({
1030 |           name,
1031 |           platform,
1032 |         }),
1033 |       },
1034 |       opts,
1035 |     );
1036 |     return ProjectSchema.parse(body);
1037 |   }
1038 | 
1039 |   /**
1040 |    * Updates an existing project's configuration.
1041 |    *
1042 |    * @param params Project update parameters
1043 |    * @param params.organizationSlug Organization identifier
1044 |    * @param params.projectSlug Current project identifier
1045 |    * @param params.name New project name (optional)
1046 |    * @param params.slug New project slug (optional)
1047 |    * @param params.platform New platform identifier (optional)
1048 |    * @param opts Request options
1049 |    * @returns Updated project data
1050 |    */
1051 |   async updateProject(
1052 |     {
1053 |       organizationSlug,
1054 |       projectSlug,
1055 |       name,
1056 |       slug,
1057 |       platform,
1058 |     }: {
1059 |       organizationSlug: string;
1060 |       projectSlug: string;
1061 |       name?: string;
1062 |       slug?: string;
1063 |       platform?: string;
1064 |     },
1065 |     opts?: RequestOptions,
1066 |   ): Promise<Project> {
1067 |     const updateData: Record<string, any> = {};
1068 |     if (name !== undefined) updateData.name = name;
1069 |     if (slug !== undefined) updateData.slug = slug;
1070 |     if (platform !== undefined) updateData.platform = platform;
1071 | 
1072 |     const body = await this.requestJSON(
1073 |       `/projects/${organizationSlug}/${projectSlug}/`,
1074 |       {
1075 |         method: "PUT",
1076 |         body: JSON.stringify(updateData),
1077 |       },
1078 |       opts,
1079 |     );
1080 |     return ProjectSchema.parse(body);
1081 |   }
1082 | 
1083 |   /**
1084 |    * Assigns a team to a project.
1085 |    *
1086 |    * @param params Assignment parameters
1087 |    * @param params.organizationSlug Organization identifier
1088 |    * @param params.projectSlug Project identifier
1089 |    * @param params.teamSlug Team identifier to assign
1090 |    * @param opts Request options
1091 |    */
1092 |   async addTeamToProject(
1093 |     {
1094 |       organizationSlug,
1095 |       projectSlug,
1096 |       teamSlug,
1097 |     }: {
1098 |       organizationSlug: string;
1099 |       projectSlug: string;
1100 |       teamSlug: string;
1101 |     },
1102 |     opts?: RequestOptions,
1103 |   ): Promise<void> {
1104 |     await this.request(
1105 |       `/projects/${organizationSlug}/${projectSlug}/teams/${teamSlug}/`,
1106 |       {
1107 |         method: "POST",
1108 |         body: JSON.stringify({}),
1109 |       },
1110 |       opts,
1111 |     );
1112 |   }
1113 | 
1114 |   /**
1115 |    * Creates a new client key (DSN) for a project.
1116 |    *
1117 |    * Client keys are used to identify and authenticate SDK requests to Sentry.
1118 |    *
1119 |    * @param params Key creation parameters
1120 |    * @param params.organizationSlug Organization identifier
1121 |    * @param params.projectSlug Project identifier
1122 |    * @param params.name Human-readable name for the key (optional)
1123 |    * @param opts Request options
1124 |    * @returns Created client key with DSN information
1125 |    *
1126 |    * @example
1127 |    * ```typescript
1128 |    * const key = await apiService.createClientKey({
1129 |    *   organizationSlug: "my-org",
1130 |    *   projectSlug: "my-project",
1131 |    *   name: "Production"
1132 |    * });
1133 |    * console.log(`DSN: ${key.dsn.public}`);
1134 |    * ```
1135 |    */
1136 |   async createClientKey(
1137 |     {
1138 |       organizationSlug,
1139 |       projectSlug,
1140 |       name,
1141 |     }: {
1142 |       organizationSlug: string;
1143 |       projectSlug: string;
1144 |       name?: string;
1145 |     },
1146 |     opts?: RequestOptions,
1147 |   ): Promise<ClientKey> {
1148 |     const body = await this.requestJSON(
1149 |       `/projects/${organizationSlug}/${projectSlug}/keys/`,
1150 |       {
1151 |         method: "POST",
1152 |         body: JSON.stringify({
1153 |           name,
1154 |         }),
1155 |       },
1156 |       opts,
1157 |     );
1158 |     return ClientKeySchema.parse(body);
1159 |   }
1160 | 
1161 |   /**
1162 |    * Lists all client keys (DSNs) for a project.
1163 |    *
1164 |    * @param params Query parameters
1165 |    * @param params.organizationSlug Organization identifier
1166 |    * @param params.projectSlug Project identifier
1167 |    * @param opts Request options
1168 |    * @returns Array of client keys with DSN information
1169 |    */
1170 |   async listClientKeys(
1171 |     {
1172 |       organizationSlug,
1173 |       projectSlug,
1174 |     }: {
1175 |       organizationSlug: string;
1176 |       projectSlug: string;
1177 |     },
1178 |     opts?: RequestOptions,
1179 |   ): Promise<ClientKeyList> {
1180 |     const body = await this.requestJSON(
1181 |       `/projects/${organizationSlug}/${projectSlug}/keys/`,
1182 |       undefined,
1183 |       opts,
1184 |     );
1185 |     return ClientKeyListSchema.parse(body);
1186 |   }
1187 | 
1188 |   /**
1189 |    * Lists releases for an organization or specific project.
1190 |    *
1191 |    * @param params Query parameters
1192 |    * @param params.organizationSlug Organization identifier
1193 |    * @param params.projectSlug Project identifier (optional, scopes to specific project)
1194 |    * @param params.query Search query for filtering releases
1195 |    * @param opts Request options
1196 |    * @returns Array of releases with deployment and commit information
1197 |    *
1198 |    * @example
1199 |    * ```typescript
1200 |    * // All releases for organization
1201 |    * const releases = await apiService.listReleases({
1202 |    *   organizationSlug: "my-org"
1203 |    * });
1204 |    *
1205 |    * // Search for specific version
1206 |    * const filtered = await apiService.listReleases({
1207 |    *   organizationSlug: "my-org",
1208 |    *   query: "v1.2.3"
1209 |    * });
1210 |    * ```
1211 |    */
1212 |   async listReleases(
1213 |     {
1214 |       organizationSlug,
1215 |       projectSlug,
1216 |       query,
1217 |     }: {
1218 |       organizationSlug: string;
1219 |       projectSlug?: string;
1220 |       query?: string;
1221 |     },
1222 |     opts?: RequestOptions,
1223 |   ): Promise<ReleaseList> {
1224 |     const searchQuery = new URLSearchParams();
1225 |     if (query) {
1226 |       searchQuery.set("query", query);
1227 |     }
1228 | 
1229 |     const path = projectSlug
1230 |       ? `/projects/${organizationSlug}/${projectSlug}/releases/`
1231 |       : `/organizations/${organizationSlug}/releases/`;
1232 | 
1233 |     const body = await this.requestJSON(
1234 |       searchQuery.toString() ? `${path}?${searchQuery.toString()}` : path,
1235 |       undefined,
1236 |       opts,
1237 |     );
1238 |     return ReleaseListSchema.parse(body);
1239 |   }
1240 | 
1241 |   /**
1242 |    * Lists available tags for search queries.
1243 |    *
1244 |    * Tags represent indexed fields that can be used in Sentry search queries.
1245 |    *
1246 |    * @param params Query parameters
1247 |    * @param params.organizationSlug Organization identifier
1248 |    * @param params.dataset Dataset to query tags for ("events", "errors" or "search_issues")
1249 |    * @param params.project Numeric project ID to filter tags
1250 |    * @param params.statsPeriod Time range for tag statistics (e.g., "24h", "7d")
1251 |    * @param params.useCache Whether to use cached results
1252 |    * @param params.useFlagsBackend Whether to use flags backend features
1253 |    * @param opts Request options
1254 |    * @returns Array of available tags with metadata
1255 |    *
1256 |    * @example
1257 |    * ```typescript
1258 |    * const tags = await apiService.listTags({
1259 |    *   organizationSlug: "my-org",
1260 |    *   dataset: "events",
1261 |    *   project: "123456",
1262 |    *   statsPeriod: "24h",
1263 |    *   useCache: true
1264 |    * });
1265 |    * tags.forEach(tag => console.log(`${tag.key}: ${tag.name}`));
1266 |    * ```
1267 |    */
1268 |   async listTags(
1269 |     {
1270 |       organizationSlug,
1271 |       dataset,
1272 |       project,
1273 |       statsPeriod,
1274 |       start,
1275 |       end,
1276 |       useCache,
1277 |       useFlagsBackend,
1278 |     }: {
1279 |       organizationSlug: string;
1280 |       dataset?: "events" | "errors" | "search_issues";
1281 |       project?: string;
1282 |       statsPeriod?: string;
1283 |       start?: string;
1284 |       end?: string;
1285 |       useCache?: boolean;
1286 |       useFlagsBackend?: boolean;
1287 |     },
1288 |     opts?: RequestOptions,
1289 |   ): Promise<TagList> {
1290 |     const searchQuery = new URLSearchParams();
1291 |     if (dataset) {
1292 |       searchQuery.set("dataset", dataset);
1293 |     }
1294 |     if (project) {
1295 |       searchQuery.set("project", project);
1296 |     }
1297 |     // Validate time parameters - can't use both relative and absolute
1298 |     if (statsPeriod && (start || end)) {
1299 |       throw new ApiValidationError(
1300 |         "Cannot use both statsPeriod and start/end parameters. Use either statsPeriod for relative time or start/end for absolute time.",
1301 |       );
1302 |     }
1303 |     if ((start && !end) || (!start && end)) {
1304 |       throw new ApiValidationError(
1305 |         "Both start and end parameters must be provided together for absolute time ranges.",
1306 |       );
1307 |     }
1308 |     // Use either relative time (statsPeriod) or absolute time (start/end)
1309 |     if (statsPeriod) {
1310 |       searchQuery.set("statsPeriod", statsPeriod);
1311 |     } else if (start && end) {
1312 |       searchQuery.set("start", start);
1313 |       searchQuery.set("end", end);
1314 |     }
1315 |     if (useCache !== undefined) {
1316 |       searchQuery.set("useCache", useCache ? "1" : "0");
1317 |     }
1318 |     if (useFlagsBackend !== undefined) {
1319 |       searchQuery.set("useFlagsBackend", useFlagsBackend ? "1" : "0");
1320 |     }
1321 | 
1322 |     const body = await this.requestJSON(
1323 |       searchQuery.toString()
1324 |         ? `/organizations/${organizationSlug}/tags/?${searchQuery.toString()}`
1325 |         : `/organizations/${organizationSlug}/tags/`,
1326 |       undefined,
1327 |       opts,
1328 |     );
1329 |     return TagListSchema.parse(body);
1330 |   }
1331 | 
1332 |   /**
1333 |    * Lists trace item attributes available for search queries.
1334 |    *
1335 |    * Returns all available fields/attributes that can be used in event searches,
1336 |    * including both built-in fields and custom tags.
1337 |    *
1338 |    * @param params Query parameters
1339 |    * @param params.organizationSlug Organization identifier
1340 |    * @param params.itemType Item type to query attributes for ("spans" or "logs")
1341 |    * @param params.project Numeric project ID to filter attributes
1342 |    * @param params.statsPeriod Time range for attribute statistics (e.g., "24h", "7d")
1343 |    * @param opts Request options
1344 |    * @returns Array of available attributes with metadata including type
1345 |    */
1346 |   async listTraceItemAttributes(
1347 |     {
1348 |       organizationSlug,
1349 |       itemType = "spans",
1350 |       project,
1351 |       statsPeriod,
1352 |       start,
1353 |       end,
1354 |     }: {
1355 |       organizationSlug: string;
1356 |       itemType?: "spans" | "logs";
1357 |       project?: string;
1358 |       statsPeriod?: string;
1359 |       start?: string;
1360 |       end?: string;
1361 |     },
1362 |     opts?: RequestOptions,
1363 |   ): Promise<Array<{ key: string; name: string; type: "string" | "number" }>> {
1364 |     // Fetch both string and number attributes
1365 |     const [stringAttributes, numberAttributes] = await Promise.all([
1366 |       this.fetchTraceItemAttributesByType(
1367 |         organizationSlug,
1368 |         itemType,
1369 |         "string",
1370 |         project,
1371 |         statsPeriod,
1372 |         start,
1373 |         end,
1374 |         opts,
1375 |       ),
1376 |       this.fetchTraceItemAttributesByType(
1377 |         organizationSlug,
1378 |         itemType,
1379 |         "number",
1380 |         project,
1381 |         statsPeriod,
1382 |         start,
1383 |         end,
1384 |         opts,
1385 |       ),
1386 |     ]);
1387 | 
1388 |     // Combine attributes with explicit type information
1389 |     const allAttributes: Array<{
1390 |       key: string;
1391 |       name: string;
1392 |       type: "string" | "number";
1393 |     }> = [];
1394 | 
1395 |     // Add string attributes
1396 |     for (const attr of stringAttributes) {
1397 |       allAttributes.push({
1398 |         key: attr.key,
1399 |         name: attr.name || attr.key,
1400 |         type: "string",
1401 |       });
1402 |     }
1403 | 
1404 |     // Add number attributes
1405 |     for (const attr of numberAttributes) {
1406 |       allAttributes.push({
1407 |         key: attr.key,
1408 |         name: attr.name || attr.key,
1409 |         type: "number",
1410 |       });
1411 |     }
1412 | 
1413 |     return allAttributes;
1414 |   }
1415 | 
1416 |   private async fetchTraceItemAttributesByType(
1417 |     organizationSlug: string,
1418 |     itemType: "spans" | "logs",
1419 |     attributeType: "string" | "number",
1420 |     project?: string,
1421 |     statsPeriod?: string,
1422 |     start?: string,
1423 |     end?: string,
1424 |     opts?: RequestOptions,
1425 |   ): Promise<any> {
1426 |     const queryParams = new URLSearchParams();
1427 |     queryParams.set("itemType", itemType);
1428 |     queryParams.set("attributeType", attributeType);
1429 |     if (project) {
1430 |       queryParams.set("project", project);
1431 |     }
1432 |     // Validate time parameters - can't use both relative and absolute
1433 |     if (statsPeriod && (start || end)) {
1434 |       throw new ApiValidationError(
1435 |         "Cannot use both statsPeriod and start/end parameters. Use either statsPeriod for relative time or start/end for absolute time.",
1436 |       );
1437 |     }
1438 |     if ((start && !end) || (!start && end)) {
1439 |       throw new ApiValidationError(
1440 |         "Both start and end parameters must be provided together for absolute time ranges.",
1441 |       );
1442 |     }
1443 |     // Use either relative time (statsPeriod) or absolute time (start/end)
1444 |     if (statsPeriod) {
1445 |       queryParams.set("statsPeriod", statsPeriod);
1446 |     } else if (start && end) {
1447 |       queryParams.set("start", start);
1448 |       queryParams.set("end", end);
1449 |     }
1450 | 
1451 |     const url = `/organizations/${organizationSlug}/trace-items/attributes/?${queryParams.toString()}`;
1452 | 
1453 |     const body = await this.requestJSON(url, undefined, opts);
1454 |     return Array.isArray(body) ? body : [];
1455 |   }
1456 | 
1457 |   /**
1458 |    * Lists issues within an organization or project.
1459 |    *
1460 |    * Issues represent groups of similar errors or problems in your application.
1461 |    * Supports Sentry's powerful query syntax for filtering and sorting.
1462 |    *
1463 |    * @param params Query parameters
1464 |    * @param params.organizationSlug Organization identifier
1465 |    * @param params.projectSlug Project identifier (optional, scopes to specific project)
1466 |    * @param params.query Sentry search query (e.g., "is:unresolved browser:chrome")
1467 |    * @param params.sortBy Sort order ("user", "freq", "date", "new")
1468 |    * @param opts Request options
1469 |    * @returns Array of issues with metadata and statistics
1470 |    *
1471 |    * @example
1472 |    * ```typescript
1473 |    * // Recent unresolved issues
1474 |    * const issues = await apiService.listIssues({
1475 |    *   organizationSlug: "my-org",
1476 |    *   query: "is:unresolved",
1477 |    *   sortBy: "date"
1478 |    * });
1479 |    *
1480 |    * // High-frequency errors in specific project
1481 |    * const critical = await apiService.listIssues({
1482 |    *   organizationSlug: "my-org",
1483 |    *   projectSlug: "backend",
1484 |    *   query: "level:error",
1485 |    *   sortBy: "freq"
1486 |    * });
1487 |    * ```
1488 |    */
1489 |   async listIssues(
1490 |     {
1491 |       organizationSlug,
1492 |       projectSlug,
1493 |       query,
1494 |       sortBy,
1495 |       limit = 10,
1496 |     }: {
1497 |       organizationSlug: string;
1498 |       projectSlug?: string;
1499 |       query?: string | null;
1500 |       sortBy?: "user" | "freq" | "date" | "new";
1501 |       limit?: number;
1502 |     },
1503 |     opts?: RequestOptions,
1504 |   ): Promise<IssueList> {
1505 |     const sentryQuery: string[] = [];
1506 |     if (query) {
1507 |       sentryQuery.push(query);
1508 |     }
1509 | 
1510 |     const queryParams = new URLSearchParams();
1511 |     queryParams.set("per_page", String(limit));
1512 |     if (sortBy) queryParams.set("sort", sortBy);
1513 |     queryParams.set("statsPeriod", "24h");
1514 |     queryParams.set("query", sentryQuery.join(" "));
1515 | 
1516 |     queryParams.append("collapse", "unhandled");
1517 | 
1518 |     const apiUrl = projectSlug
1519 |       ? `/projects/${organizationSlug}/${projectSlug}/issues/?${queryParams.toString()}`
1520 |       : `/organizations/${organizationSlug}/issues/?${queryParams.toString()}`;
1521 | 
1522 |     const body = await this.requestJSON(apiUrl, undefined, opts);
1523 |     return IssueListSchema.parse(body);
1524 |   }
1525 | 
1526 |   async getIssue(
1527 |     {
1528 |       organizationSlug,
1529 |       issueId,
1530 |     }: {
1531 |       organizationSlug: string;
1532 |       issueId: string;
1533 |     },
1534 |     opts?: RequestOptions,
1535 |   ): Promise<Issue> {
1536 |     const body = await this.requestJSON(
1537 |       `/organizations/${organizationSlug}/issues/${issueId}/`,
1538 |       undefined,
1539 |       opts,
1540 |     );
1541 |     return IssueSchema.parse(body);
1542 |   }
1543 | 
1544 |   async getEventForIssue(
1545 |     {
1546 |       organizationSlug,
1547 |       issueId,
1548 |       eventId,
1549 |     }: {
1550 |       organizationSlug: string;
1551 |       issueId: string;
1552 |       eventId: string;
1553 |     },
1554 |     opts?: RequestOptions,
1555 |   ): Promise<Event> {
1556 |     const body = await this.requestJSON(
1557 |       `/organizations/${organizationSlug}/issues/${issueId}/events/${eventId}/`,
1558 |       undefined,
1559 |       opts,
1560 |     );
1561 |     const rawEvent = EventSchema.parse(body);
1562 | 
1563 |     // Filter out unknown events - only return known error/default/transaction types
1564 |     // "default" type represents error events without exception data
1565 |     if (rawEvent.type === "error" || rawEvent.type === "default") {
1566 |       return rawEvent as Event;
1567 |     }
1568 |     if (rawEvent.type === "transaction") {
1569 |       return rawEvent as Event;
1570 |     }
1571 | 
1572 |     const eventType =
1573 |       typeof rawEvent.type === "string" ? rawEvent.type : String(rawEvent.type);
1574 |     throw new ApiValidationError(
1575 |       `Unknown event type: ${eventType}`,
1576 |       400,
1577 |       `Only error, default, and transaction events are supported, got: ${eventType}`,
1578 |       body,
1579 |     );
1580 |   }
1581 | 
1582 |   async getLatestEventForIssue(
1583 |     {
1584 |       organizationSlug,
1585 |       issueId,
1586 |     }: {
1587 |       organizationSlug: string;
1588 |       issueId: string;
1589 |     },
1590 |     opts?: RequestOptions,
1591 |   ): Promise<Event> {
1592 |     return this.getEventForIssue(
1593 |       {
1594 |         organizationSlug,
1595 |         issueId,
1596 |         eventId: "latest",
1597 |       },
1598 |       opts,
1599 |     );
1600 |   }
1601 | 
1602 |   async listEventAttachments(
1603 |     {
1604 |       organizationSlug,
1605 |       projectSlug,
1606 |       eventId,
1607 |     }: {
1608 |       organizationSlug: string;
1609 |       projectSlug: string;
1610 |       eventId: string;
1611 |     },
1612 |     opts?: RequestOptions,
1613 |   ): Promise<EventAttachmentList> {
1614 |     const body = await this.requestJSON(
1615 |       `/projects/${organizationSlug}/${projectSlug}/events/${eventId}/attachments/`,
1616 |       undefined,
1617 |       opts,
1618 |     );
1619 |     return EventAttachmentListSchema.parse(body);
1620 |   }
1621 | 
1622 |   async getEventAttachment(
1623 |     {
1624 |       organizationSlug,
1625 |       projectSlug,
1626 |       eventId,
1627 |       attachmentId,
1628 |     }: {
1629 |       organizationSlug: string;
1630 |       projectSlug: string;
1631 |       eventId: string;
1632 |       attachmentId: string;
1633 |     },
1634 |     opts?: RequestOptions,
1635 |   ): Promise<{
1636 |     attachment: EventAttachment;
1637 |     downloadUrl: string;
1638 |     filename: string;
1639 |     blob: Blob;
1640 |   }> {
1641 |     // Get the attachment metadata first
1642 |     const attachmentsData = await this.requestJSON(
1643 |       `/projects/${organizationSlug}/${projectSlug}/events/${eventId}/attachments/`,
1644 |       undefined,
1645 |       opts,
1646 |     );
1647 | 
1648 |     const attachments = EventAttachmentListSchema.parse(attachmentsData);
1649 |     const attachment = attachments.find((att) => att.id === attachmentId);
1650 | 
1651 |     if (!attachment) {
1652 |       throw new ApiNotFoundError(
1653 |         `Attachment with ID ${attachmentId} not found for event ${eventId}`,
1654 |       );
1655 |     }
1656 | 
1657 |     // Download the actual file content
1658 |     const downloadUrl = `/projects/${organizationSlug}/${projectSlug}/events/${eventId}/attachments/${attachmentId}/?download=1`;
1659 |     const downloadResponse = await this.request(
1660 |       downloadUrl,
1661 |       {
1662 |         method: "GET",
1663 |         headers: {
1664 |           Accept: "application/octet-stream",
1665 |         },
1666 |       },
1667 |       opts,
1668 |     );
1669 | 
1670 |     return {
1671 |       attachment,
1672 |       downloadUrl: downloadResponse.url,
1673 |       filename: attachment.name,
1674 |       blob: await downloadResponse.blob(),
1675 |     };
1676 |   }
1677 | 
1678 |   async updateIssue(
1679 |     {
1680 |       organizationSlug,
1681 |       issueId,
1682 |       status,
1683 |       assignedTo,
1684 |     }: {
1685 |       organizationSlug: string;
1686 |       issueId: string;
1687 |       status?: string;
1688 |       assignedTo?: string;
1689 |     },
1690 |     opts?: RequestOptions,
1691 |   ): Promise<Issue> {
1692 |     const updateData: Record<string, any> = {};
1693 |     if (status !== undefined) updateData.status = status;
1694 |     if (assignedTo !== undefined) updateData.assignedTo = assignedTo;
1695 | 
1696 |     const body = await this.requestJSON(
1697 |       `/organizations/${organizationSlug}/issues/${issueId}/`,
1698 |       {
1699 |         method: "PUT",
1700 |         body: JSON.stringify(updateData),
1701 |       },
1702 |       opts,
1703 |     );
1704 |     return IssueSchema.parse(body);
1705 |   }
1706 | 
1707 |   // TODO: Sentry is not yet exposing a reasonable API to fetch trace data
1708 |   // async getTrace({
1709 |   //   organizationSlug,
1710 |   //   traceId,
1711 |   // }: {
1712 |   //   organizationSlug: string;
1713 |   //   traceId: string;
1714 |   // }): Promise<z.infer<typeof SentryIssueSchema>> {
1715 |   //   const response = await this.request(
1716 |   //     `/organizations/${organizationSlug}/issues/${traceId}/`,
1717 |   //   );
1718 | 
1719 |   //   const body = await response.json();
1720 |   //   return SentryIssueSchema.parse(body);
1721 |   // }
1722 | 
1723 |   async searchErrors(
1724 |     {
1725 |       organizationSlug,
1726 |       projectSlug,
1727 |       filename,
1728 |       transaction,
1729 |       query,
1730 |       sortBy = "last_seen",
1731 |     }: {
1732 |       organizationSlug: string;
1733 |       projectSlug?: string;
1734 |       filename?: string;
1735 |       transaction?: string;
1736 |       query?: string;
1737 |       sortBy?: "last_seen" | "count";
1738 |     },
1739 |     opts?: RequestOptions,
1740 |   ) {
1741 |     const sentryQuery: string[] = [];
1742 |     if (filename) {
1743 |       sentryQuery.push(`stack.filename:"*${filename.replace(/"/g, '\\"')}"`);
1744 |     }
1745 |     if (transaction) {
1746 |       sentryQuery.push(`transaction:"${transaction.replace(/"/g, '\\"')}"`);
1747 |     }
1748 |     if (query) {
1749 |       sentryQuery.push(query);
1750 |     }
1751 |     if (projectSlug) {
1752 |       sentryQuery.push(`project:${projectSlug}`);
1753 |     }
1754 | 
1755 |     const queryParams = new URLSearchParams();
1756 |     queryParams.set("dataset", "errors");
1757 |     queryParams.set("per_page", "10");
1758 |     queryParams.set(
1759 |       "sort",
1760 |       `-${sortBy === "last_seen" ? "last_seen" : "count"}`,
1761 |     );
1762 |     queryParams.set("statsPeriod", "24h");
1763 |     queryParams.append("field", "issue");
1764 |     queryParams.append("field", "title");
1765 |     queryParams.append("field", "project");
1766 |     queryParams.append("field", "last_seen()");
1767 |     queryParams.append("field", "count()");
1768 |     queryParams.set("query", sentryQuery.join(" "));
1769 |     // if (projectSlug) queryParams.set("project", projectSlug);
1770 | 
1771 |     const apiUrl = `/organizations/${organizationSlug}/events/?${queryParams.toString()}`;
1772 | 
1773 |     const body = await this.requestJSON(apiUrl, undefined, opts);
1774 |     // TODO(dcramer): If you're using an older version of Sentry this API had a breaking change
1775 |     // meaning this endpoint will error.
1776 |     return ErrorsSearchResponseSchema.parse(body).data;
1777 |   }
1778 | 
1779 |   async searchSpans(
1780 |     {
1781 |       organizationSlug,
1782 |       projectSlug,
1783 |       transaction,
1784 |       query,
1785 |       sortBy = "timestamp",
1786 |     }: {
1787 |       organizationSlug: string;
1788 |       projectSlug?: string;
1789 |       transaction?: string;
1790 |       query?: string;
1791 |       sortBy?: "timestamp" | "duration";
1792 |     },
1793 |     opts?: RequestOptions,
1794 |   ) {
1795 |     const sentryQuery: string[] = ["is_transaction:true"];
1796 |     if (transaction) {
1797 |       sentryQuery.push(`transaction:"${transaction.replace(/"/g, '\\"')}"`);
1798 |     }
1799 |     if (query) {
1800 |       sentryQuery.push(query);
1801 |     }
1802 |     if (projectSlug) {
1803 |       sentryQuery.push(`project:${projectSlug}`);
1804 |     }
1805 | 
1806 |     const queryParams = new URLSearchParams();
1807 |     queryParams.set("dataset", "spans");
1808 |     queryParams.set("per_page", "10");
1809 |     queryParams.set(
1810 |       "sort",
1811 |       `-${sortBy === "timestamp" ? "timestamp" : "span.duration"}`,
1812 |     );
1813 |     queryParams.set("allowAggregateConditions", "0");
1814 |     queryParams.set("useRpc", "1");
1815 |     queryParams.append("field", "id");
1816 |     queryParams.append("field", "trace");
1817 |     queryParams.append("field", "span.op");
1818 |     queryParams.append("field", "span.description");
1819 |     queryParams.append("field", "span.duration");
1820 |     queryParams.append("field", "transaction");
1821 |     queryParams.append("field", "project");
1822 |     queryParams.append("field", "timestamp");
1823 |     queryParams.set("query", sentryQuery.join(" "));
1824 |     // if (projectSlug) queryParams.set("project", projectSlug);
1825 | 
1826 |     const apiUrl = `/organizations/${organizationSlug}/events/?${queryParams.toString()}`;
1827 | 
1828 |     const body = await this.requestJSON(apiUrl, undefined, opts);
1829 |     return SpansSearchResponseSchema.parse(body).data;
1830 |   }
1831 | 
1832 |   // ================================================================================
1833 |   // API QUERY BUILDERS FOR DIFFERENT SENTRY APIS
1834 |   // ================================================================================
1835 | 
1836 |   /**
1837 |    * Builds query parameters for the legacy Discover API (primarily used by errors dataset).
1838 |    *
1839 |    * Note: While the API endpoint is the same for all datasets, we maintain separate
1840 |    * builders to make future divergence easier and to keep the code organized.
1841 |    */
1842 |   private buildDiscoverApiQuery(params: {
1843 |     query: string;
1844 |     fields: string[];
1845 |     limit: number;
1846 |     projectId?: string;
1847 |     statsPeriod?: string;
1848 |     start?: string;
1849 |     end?: string;
1850 |     sort: string;
1851 |   }): URLSearchParams {
1852 |     const queryParams = new URLSearchParams();
1853 | 
1854 |     // Basic parameters
1855 |     queryParams.set("per_page", params.limit.toString());
1856 |     queryParams.set("query", params.query);
1857 |     queryParams.set("dataset", "errors");
1858 | 
1859 |     // Validate time parameters - can't use both relative and absolute
1860 |     if (params.statsPeriod && (params.start || params.end)) {
1861 |       throw new ApiValidationError(
1862 |         "Cannot use both statsPeriod and start/end parameters. Use either statsPeriod for relative time or start/end for absolute time.",
1863 |       );
1864 |     }
1865 |     if ((params.start && !params.end) || (!params.start && params.end)) {
1866 |       throw new ApiValidationError(
1867 |         "Both start and end parameters must be provided together for absolute time ranges.",
1868 |       );
1869 |     }
1870 |     // Use either relative time (statsPeriod) or absolute time (start/end)
1871 |     if (params.statsPeriod) {
1872 |       queryParams.set("statsPeriod", params.statsPeriod);
1873 |     } else if (params.start && params.end) {
1874 |       queryParams.set("start", params.start);
1875 |       queryParams.set("end", params.end);
1876 |     }
1877 | 
1878 |     if (params.projectId) {
1879 |       queryParams.set("project", params.projectId);
1880 |     }
1881 | 
1882 |     // Sort parameter transformation for API compatibility
1883 |     let apiSort = params.sort;
1884 |     // Skip transformation for equation fields - they should be passed as-is
1885 |     if (params.sort?.includes("(") && !params.sort?.includes("equation|")) {
1886 |       // Transform: count(field) -> count_field, count() -> count
1887 |       // Use safer string manipulation to avoid ReDoS
1888 |       const parenStart = params.sort.indexOf("(");
1889 |       const parenEnd = params.sort.indexOf(")", parenStart);
1890 |       if (parenStart !== -1 && parenEnd !== -1) {
1891 |         const beforeParen = params.sort.substring(0, parenStart);
1892 |         const insideParen = params.sort.substring(parenStart + 1, parenEnd);
1893 |         const afterParen = params.sort.substring(parenEnd + 1);
1894 |         const transformedInside = insideParen
1895 |           ? `_${insideParen.replace(/\./g, "_")}`
1896 |           : "";
1897 |         apiSort = beforeParen + transformedInside + afterParen;
1898 |       }
1899 |     }
1900 |     queryParams.set("sort", apiSort);
1901 | 
1902 |     // Add fields
1903 |     for (const field of params.fields) {
1904 |       queryParams.append("field", field);
1905 |     }
1906 | 
1907 |     return queryParams;
1908 |   }
1909 | 
1910 |   /**
1911 |    * Builds query parameters for the modern EAP API (used by spans/logs datasets).
1912 |    *
1913 |    * Includes dataset-specific parameters like sampling for spans.
1914 |    */
1915 |   private buildEapApiQuery(params: {
1916 |     query: string;
1917 |     fields: string[];
1918 |     limit: number;
1919 |     projectId?: string;
1920 |     dataset: "spans" | "ourlogs";
1921 |     statsPeriod?: string;
1922 |     start?: string;
1923 |     end?: string;
1924 |     sort: string;
1925 |   }): URLSearchParams {
1926 |     const queryParams = new URLSearchParams();
1927 | 
1928 |     // Basic parameters
1929 |     queryParams.set("per_page", params.limit.toString());
1930 |     queryParams.set("query", params.query);
1931 |     queryParams.set("dataset", params.dataset);
1932 | 
1933 |     // Validate time parameters - can't use both relative and absolute
1934 |     if (params.statsPeriod && (params.start || params.end)) {
1935 |       throw new ApiValidationError(
1936 |         "Cannot use both statsPeriod and start/end parameters. Use either statsPeriod for relative time or start/end for absolute time.",
1937 |       );
1938 |     }
1939 |     if ((params.start && !params.end) || (!params.start && params.end)) {
1940 |       throw new ApiValidationError(
1941 |         "Both start and end parameters must be provided together for absolute time ranges.",
1942 |       );
1943 |     }
1944 |     // Use either relative time (statsPeriod) or absolute time (start/end)
1945 |     if (params.statsPeriod) {
1946 |       queryParams.set("statsPeriod", params.statsPeriod);
1947 |     } else if (params.start && params.end) {
1948 |       queryParams.set("start", params.start);
1949 |       queryParams.set("end", params.end);
1950 |     }
1951 | 
1952 |     if (params.projectId) {
1953 |       queryParams.set("project", params.projectId);
1954 |     }
1955 | 
1956 |     // Dataset-specific parameters
1957 |     if (params.dataset === "spans") {
1958 |       queryParams.set("sampling", "NORMAL");
1959 |     }
1960 | 
1961 |     // Sort parameter transformation for API compatibility
1962 |     let apiSort = params.sort;
1963 |     // Skip transformation for equation fields - they should be passed as-is
1964 |     if (params.sort?.includes("(") && !params.sort?.includes("equation|")) {
1965 |       // Transform: count(field) -> count_field, count() -> count
1966 |       // Use safer string manipulation to avoid ReDoS
1967 |       const parenStart = params.sort.indexOf("(");
1968 |       const parenEnd = params.sort.indexOf(")", parenStart);
1969 |       if (parenStart !== -1 && parenEnd !== -1) {
1970 |         const beforeParen = params.sort.substring(0, parenStart);
1971 |         const insideParen = params.sort.substring(parenStart + 1, parenEnd);
1972 |         const afterParen = params.sort.substring(parenEnd + 1);
1973 |         const transformedInside = insideParen
1974 |           ? `_${insideParen.replace(/\./g, "_")}`
1975 |           : "";
1976 |         apiSort = beforeParen + transformedInside + afterParen;
1977 |       }
1978 |     }
1979 |     queryParams.set("sort", apiSort);
1980 | 
1981 |     // Add fields
1982 |     for (const field of params.fields) {
1983 |       queryParams.append("field", field);
1984 |     }
1985 | 
1986 |     return queryParams;
1987 |   }
1988 | 
1989 |   /**
1990 |    * Searches for events in Sentry using the unified events API.
1991 |    * This method is used by the search_events tool for semantic search.
1992 |    *
1993 |    * Routes to the appropriate query builder based on dataset, even though
1994 |    * the underlying API endpoint is the same. This separation makes the code
1995 |    * cleaner and allows for future API divergence.
1996 |    */
1997 |   async searchEvents(
1998 |     {
1999 |       organizationSlug,
2000 |       query,
2001 |       fields,
2002 |       limit = 10,
2003 |       projectId,
2004 |       dataset = "spans",
2005 |       statsPeriod,
2006 |       start,
2007 |       end,
2008 |       sort = "-timestamp",
2009 |     }: {
2010 |       organizationSlug: string;
2011 |       query: string;
2012 |       fields: string[];
2013 |       limit?: number;
2014 |       projectId?: string;
2015 |       dataset?: "spans" | "errors" | "ourlogs";
2016 |       statsPeriod?: string;
2017 |       start?: string;
2018 |       end?: string;
2019 |       sort?: string;
2020 |     },
2021 |     opts?: RequestOptions,
2022 |   ) {
2023 |     let queryParams: URLSearchParams;
2024 | 
2025 |     if (dataset === "errors") {
2026 |       // Use Discover API query builder
2027 |       queryParams = this.buildDiscoverApiQuery({
2028 |         query,
2029 |         fields,
2030 |         limit,
2031 |         projectId,
2032 |         statsPeriod,
2033 |         start,
2034 |         end,
2035 |         sort,
2036 |       });
2037 |     } else {
2038 |       // Use EAP API query builder for spans and logs
2039 |       queryParams = this.buildEapApiQuery({
2040 |         query,
2041 |         fields,
2042 |         limit,
2043 |         projectId,
2044 |         dataset,
2045 |         statsPeriod,
2046 |         start,
2047 |         end,
2048 |         sort,
2049 |       });
2050 |     }
2051 | 
2052 |     const apiUrl = `/organizations/${organizationSlug}/events/?${queryParams.toString()}`;
2053 |     return await this.requestJSON(apiUrl, undefined, opts);
2054 |   }
2055 | 
2056 |   // POST https://us.sentry.io/api/0/issues/5485083130/autofix/
2057 |   async startAutofix(
2058 |     {
2059 |       organizationSlug,
2060 |       issueId,
2061 |       eventId,
2062 |       instruction = "",
2063 |     }: {
2064 |       organizationSlug: string;
2065 |       issueId: string;
2066 |       eventId?: string;
2067 |       instruction?: string;
2068 |     },
2069 |     opts?: RequestOptions,
2070 |   ): Promise<AutofixRun> {
2071 |     const body = await this.requestJSON(
2072 |       `/organizations/${organizationSlug}/issues/${issueId}/autofix/`,
2073 |       {
2074 |         method: "POST",
2075 |         body: JSON.stringify({
2076 |           event_id: eventId,
2077 |           instruction,
2078 |         }),
2079 |       },
2080 |       opts,
2081 |     );
2082 |     return AutofixRunSchema.parse(body);
2083 |   }
2084 | 
2085 |   // GET https://us.sentry.io/api/0/issues/5485083130/autofix/
2086 |   async getAutofixState(
2087 |     {
2088 |       organizationSlug,
2089 |       issueId,
2090 |     }: {
2091 |       organizationSlug: string;
2092 |       issueId: string;
2093 |     },
2094 |     opts?: RequestOptions,
2095 |   ): Promise<AutofixRunState> {
2096 |     const body = await this.requestJSON(
2097 |       `/organizations/${organizationSlug}/issues/${issueId}/autofix/`,
2098 |       undefined,
2099 |       opts,
2100 |     );
2101 |     return AutofixRunStateSchema.parse(body);
2102 |   }
2103 | 
2104 |   /**
2105 |    * Retrieves high-level metadata about a trace.
2106 |    *
2107 |    * Returns statistics including span counts, error counts, transaction
2108 |    * breakdown, and operation type distribution for the specified trace.
2109 |    *
2110 |    * @param params Query parameters
2111 |    * @param params.organizationSlug Organization identifier
2112 |    * @param params.traceId Trace identifier (32-character hex string)
2113 |    * @param params.statsPeriod Optional stats period (e.g., "14d", "7d")
2114 |    * @param opts Request options
2115 |    * @returns Trace metadata with statistics
2116 |    *
2117 |    * @example
2118 |    * ```typescript
2119 |    * const traceMeta = await apiService.getTraceMeta({
2120 |    *   organizationSlug: "my-org",
2121 |    *   traceId: "a4d1aae7216b47ff8117cf4e09ce9d0a"
2122 |    * });
2123 |    * console.log(`Trace has ${traceMeta.span_count} spans`);
2124 |    * ```
2125 |    */
2126 |   async getTraceMeta(
2127 |     {
2128 |       organizationSlug,
2129 |       traceId,
2130 |       statsPeriod = "14d",
2131 |     }: {
2132 |       organizationSlug: string;
2133 |       traceId: string;
2134 |       statsPeriod?: string;
2135 |     },
2136 |     opts?: RequestOptions,
2137 |   ): Promise<TraceMeta> {
2138 |     const queryParams = new URLSearchParams();
2139 |     queryParams.set("statsPeriod", statsPeriod);
2140 | 
2141 |     const body = await this.requestJSON(
2142 |       `/organizations/${organizationSlug}/trace-meta/${traceId}/?${queryParams.toString()}`,
2143 |       undefined,
2144 |       opts,
2145 |     );
2146 |     return TraceMetaSchema.parse(body);
2147 |   }
2148 | 
2149 |   /**
2150 |    * Retrieves the complete trace structure with all spans.
2151 |    *
2152 |    * Returns the hierarchical trace data including all spans, their timing
2153 |    * information, operation details, and nested relationships.
2154 |    *
2155 |    * @param params Query parameters
2156 |    * @param params.organizationSlug Organization identifier
2157 |    * @param params.traceId Trace identifier (32-character hex string)
2158 |    * @param params.limit Maximum number of spans to return (default: 1000)
2159 |    * @param params.project Project filter (-1 for all projects)
2160 |    * @param params.statsPeriod Optional stats period (e.g., "14d", "7d")
2161 |    * @param opts Request options
2162 |    * @returns Complete trace tree structure
2163 |    *
2164 |    * @example
2165 |    * ```typescript
2166 |    * const trace = await apiService.getTrace({
2167 |    *   organizationSlug: "my-org",
2168 |    *   traceId: "a4d1aae7216b47ff8117cf4e09ce9d0a",
2169 |    *   limit: 1000
2170 |    * });
2171 |    * console.log(`Root spans: ${trace.length}`);
2172 |    * ```
2173 |    */
2174 |   async getTrace(
2175 |     {
2176 |       organizationSlug,
2177 |       traceId,
2178 |       limit = 1000,
2179 |       project = "-1",
2180 |       statsPeriod = "14d",
2181 |     }: {
2182 |       organizationSlug: string;
2183 |       traceId: string;
2184 |       limit?: number;
2185 |       project?: string;
2186 |       statsPeriod?: string;
2187 |     },
2188 |     opts?: RequestOptions,
2189 |   ): Promise<Trace> {
2190 |     const queryParams = new URLSearchParams();
2191 |     queryParams.set("limit", String(limit));
2192 |     queryParams.set("project", project);
2193 |     queryParams.set("statsPeriod", statsPeriod);
2194 | 
2195 |     const body = await this.requestJSON(
2196 |       `/organizations/${organizationSlug}/trace/${traceId}/?${queryParams.toString()}`,
2197 |       undefined,
2198 |       opts,
2199 |     );
2200 |     return TraceSchema.parse(body);
2201 |   }
2202 | }
2203 | 
```
Page 14/15FirstPrevNextLast