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

# Directory Structure

```
├── .claude
│   ├── agents
│   │   └── claude-optimizer.md
│   ├── commands
│   │   ├── gh-pr.md
│   │   └── gh-review.md
│   └── settings.json
├── .craft.yml
├── .cursor
│   └── mcp.json
├── .env.example
├── .github
│   └── workflows
│       ├── deploy.yml
│       ├── eval.yml
│       ├── merge-jobs.yml
│       ├── release.yml
│       ├── smoke-tests.yml
│       ├── test.yml
│       └── token-cost.yml
├── .gitignore
├── .mcp.json
├── .vscode
│   ├── extensions.json
│   ├── mcp.json
│   └── settings.json
├── AGENTS.md
├── benchmark-agent.sh
├── bin
│   └── bump-version.sh
├── biome.json
├── CLAUDE.md
├── codecov.yml
├── core
├── docs
│   ├── adding-tools.md
│   ├── api-patterns.md
│   ├── architecture.md
│   ├── cloudflare
│   │   ├── architecture.md
│   │   ├── oauth-architecture.md
│   │   └── overview.md
│   ├── coding-guidelines.md
│   ├── common-patterns.md
│   ├── error-handling.md
│   ├── github-actions.md
│   ├── llms
│   │   ├── document-scopes.md
│   │   ├── documentation-style-guide.md
│   │   └── README.md
│   ├── logging.md
│   ├── monitoring.md
│   ├── pr-management.md
│   ├── quality-checks.md
│   ├── README.md
│   ├── releases
│   │   ├── cloudflare.md
│   │   └── stdio.md
│   ├── search-events-api-patterns.md
│   ├── security.md
│   ├── specs
│   │   ├── README.md
│   │   ├── search-events.md
│   │   └── subpath-constraints.md
│   ├── testing-remote.md
│   ├── testing-stdio.md
│   ├── testing.md
│   └── token-cost-tracking.md
├── LICENSE.md
├── Makefile
├── package.json
├── packages
│   ├── mcp-cloudflare
│   │   ├── .env.example
│   │   ├── components.json
│   │   ├── index.html
│   │   ├── package.json
│   │   ├── public
│   │   │   ├── demo.cast
│   │   │   ├── favicon.ico
│   │   │   ├── flow-transparent.png
│   │   │   ├── flow.jpg
│   │   │   ├── keycap-⌘.png
│   │   │   ├── keycap-c.png
│   │   │   └── keycap-v.png
│   │   ├── src
│   │   │   ├── client
│   │   │   │   ├── app.tsx
│   │   │   │   ├── components
│   │   │   │   │   ├── animation
│   │   │   │   │   │   ├── browser-ui
│   │   │   │   │   │   │   ├── BrowserWindow.tsx
│   │   │   │   │   │   │   ├── BrowserWindowIconSidebar.tsx
│   │   │   │   │   │   │   ├── DiffBlock.tsx
│   │   │   │   │   │   │   ├── IDEWindow.tsx
│   │   │   │   │   │   │   ├── IssueDetails.tsx
│   │   │   │   │   │   │   ├── keys-copy.tsx
│   │   │   │   │   │   │   ├── LoadingSquares.tsx
│   │   │   │   │   │   │   ├── RootCause.tsx
│   │   │   │   │   │   │   ├── seer-clipmask.tsx
│   │   │   │   │   │   │   ├── seer-noisefilter.tsx
│   │   │   │   │   │   │   ├── seer.tsx
│   │   │   │   │   │   │   └── WindowHeader.tsx
│   │   │   │   │   │   ├── BrowserAnimation.tsx
│   │   │   │   │   │   ├── DataWire.tsx
│   │   │   │   │   │   ├── dracula.css
│   │   │   │   │   │   ├── terminal-ui
│   │   │   │   │   │   │   ├── keys-paste.tsx
│   │   │   │   │   │   │   ├── SpeedDisplay.tsx
│   │   │   │   │   │   │   └── StepsList.tsx
│   │   │   │   │   │   ├── TerminalAnimation.tsx
│   │   │   │   │   │   └── tests.tsx
│   │   │   │   │   ├── chat
│   │   │   │   │   │   ├── auth-form.tsx
│   │   │   │   │   │   ├── chat-input.tsx
│   │   │   │   │   │   ├── chat-message.tsx
│   │   │   │   │   │   ├── chat-messages.tsx
│   │   │   │   │   │   ├── chat-ui.tsx
│   │   │   │   │   │   ├── chat.tsx
│   │   │   │   │   │   ├── index.ts
│   │   │   │   │   │   ├── tool-invocation.tsx
│   │   │   │   │   │   └── types.ts
│   │   │   │   │   ├── fragments
│   │   │   │   │   │   ├── install-tabs.tsx
│   │   │   │   │   │   ├── remote-setup.tsx
│   │   │   │   │   │   ├── setup-guide.tsx
│   │   │   │   │   │   └── stdio-setup.tsx
│   │   │   │   │   ├── getting-started.tsx
│   │   │   │   │   ├── hero
│   │   │   │   │   │   ├── header-divider.tsx
│   │   │   │   │   │   └── hero-block.tsx
│   │   │   │   │   ├── home-layout
│   │   │   │   │   │   ├── footer.tsx
│   │   │   │   │   │   └── sidebars.tsx
│   │   │   │   │   ├── ui
│   │   │   │   │   │   ├── accordion.tsx
│   │   │   │   │   │   ├── backdrop.tsx
│   │   │   │   │   │   ├── badge.tsx
│   │   │   │   │   │   ├── base.tsx
│   │   │   │   │   │   ├── button.tsx
│   │   │   │   │   │   ├── code-snippet.tsx
│   │   │   │   │   │   ├── header.tsx
│   │   │   │   │   │   ├── icon.tsx
│   │   │   │   │   │   ├── icons
│   │   │   │   │   │   │   ├── claude.tsx
│   │   │   │   │   │   │   ├── codex.tsx
│   │   │   │   │   │   │   ├── cursor.tsx
│   │   │   │   │   │   │   ├── gemini.tsx
│   │   │   │   │   │   │   ├── sentry.tsx
│   │   │   │   │   │   │   ├── vscode.tsx
│   │   │   │   │   │   │   ├── warp.tsx
│   │   │   │   │   │   │   ├── windsurf.tsx
│   │   │   │   │   │   │   └── zed.tsx
│   │   │   │   │   │   ├── interactive-markdown.tsx
│   │   │   │   │   │   ├── json-schema-params.tsx
│   │   │   │   │   │   ├── key-icon.tsx
│   │   │   │   │   │   ├── key-word.tsx
│   │   │   │   │   │   ├── markdown.tsx
│   │   │   │   │   │   ├── note.tsx
│   │   │   │   │   │   ├── prose.tsx
│   │   │   │   │   │   ├── section.tsx
│   │   │   │   │   │   ├── slash-command-actions.tsx
│   │   │   │   │   │   ├── slash-command-text.tsx
│   │   │   │   │   │   ├── sliding-panel.tsx
│   │   │   │   │   │   ├── template-vars.tsx
│   │   │   │   │   │   ├── tool-actions.tsx
│   │   │   │   │   │   └── typewriter.tsx
│   │   │   │   │   └── usecases
│   │   │   │   │       ├── fix-bugs.tsx
│   │   │   │   │       ├── index.tsx
│   │   │   │   │       ├── instrument.tsx
│   │   │   │   │       ├── search-things.tsx
│   │   │   │   │       └── search-visual.tsx
│   │   │   │   ├── contexts
│   │   │   │   │   └── auth-context.tsx
│   │   │   │   ├── hooks
│   │   │   │   │   ├── use-endpoint-mode.ts
│   │   │   │   │   ├── use-mcp-metadata.ts
│   │   │   │   │   ├── use-persisted-chat.ts
│   │   │   │   │   ├── use-scroll-lock.ts
│   │   │   │   │   └── use-streaming-simulation.ts
│   │   │   │   ├── index.css
│   │   │   │   ├── instrument.ts
│   │   │   │   ├── lib
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── main.tsx
│   │   │   │   ├── utils
│   │   │   │   │   ├── chat-error-handler.ts
│   │   │   │   │   ├── cursor-deeplink.ts
│   │   │   │   │   └── index.ts
│   │   │   │   └── vite-env.d.ts
│   │   │   ├── constants.ts
│   │   │   ├── server
│   │   │   │   ├── app.test.ts
│   │   │   │   ├── app.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── lib
│   │   │   │   │   ├── approval-dialog.test.ts
│   │   │   │   │   ├── approval-dialog.ts
│   │   │   │   │   ├── constraint-utils.test.ts
│   │   │   │   │   ├── constraint-utils.ts
│   │   │   │   │   ├── html-utils.ts
│   │   │   │   │   ├── mcp-handler.test.ts
│   │   │   │   │   ├── mcp-handler.ts
│   │   │   │   │   └── slug-validation.ts
│   │   │   │   ├── logging.ts
│   │   │   │   ├── oauth
│   │   │   │   │   ├── authorize.test.ts
│   │   │   │   │   ├── callback.test.ts
│   │   │   │   │   ├── constants.ts
│   │   │   │   │   ├── helpers.test.ts
│   │   │   │   │   ├── helpers.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── routes
│   │   │   │   │   │   ├── authorize.ts
│   │   │   │   │   │   ├── callback.ts
│   │   │   │   │   │   └── index.ts
│   │   │   │   │   └── state.ts
│   │   │   │   ├── routes
│   │   │   │   │   ├── chat-oauth.ts
│   │   │   │   │   ├── chat.ts
│   │   │   │   │   ├── mcp.ts
│   │   │   │   │   ├── metadata.ts
│   │   │   │   │   ├── search.test.ts
│   │   │   │   │   └── search.ts
│   │   │   │   ├── sentry.config.ts
│   │   │   │   ├── types
│   │   │   │   │   └── chat.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── utils
│   │   │   │       ├── auth-errors.ts
│   │   │   │       ├── client-ip.test.ts
│   │   │   │       ├── client-ip.ts
│   │   │   │       ├── rate-limiter.test.ts
│   │   │   │       └── rate-limiter.ts
│   │   │   └── test-setup.ts
│   │   ├── tsconfig.client.json
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   ├── tsconfig.server.json
│   │   ├── vite.config.ts
│   │   ├── vitest.config.ts
│   │   ├── worker-configuration.d.ts
│   │   ├── wrangler.canary.jsonc
│   │   └── wrangler.jsonc
│   ├── mcp-core
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── scripts
│   │   │   ├── generate-definitions.ts
│   │   │   ├── generate-otel-namespaces.ts
│   │   │   ├── measure-token-cost.ts
│   │   │   └── validate-skills-mapping.ts
│   │   ├── src
│   │   │   ├── api-client
│   │   │   │   ├── client.test.ts
│   │   │   │   ├── client.ts
│   │   │   │   ├── errors.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── schema.test.ts
│   │   │   │   ├── schema.ts
│   │   │   │   └── types.ts
│   │   │   ├── constants.ts
│   │   │   ├── errors.test.ts
│   │   │   ├── errors.ts
│   │   │   ├── internal
│   │   │   │   ├── agents
│   │   │   │   │   ├── callEmbeddedAgent.ts
│   │   │   │   │   ├── openai-provider.test.ts
│   │   │   │   │   ├── openai-provider.ts
│   │   │   │   │   └── tools
│   │   │   │   │       ├── data
│   │   │   │   │       │   ├── __namespaces.json
│   │   │   │   │       │   ├── android.json
│   │   │   │   │       │   ├── app.json
│   │   │   │   │       │   ├── artifact.json
│   │   │   │   │       │   ├── aspnetcore.json
│   │   │   │   │       │   ├── aws.json
│   │   │   │   │       │   ├── azure.json
│   │   │   │   │       │   ├── browser.json
│   │   │   │   │       │   ├── cassandra.json
│   │   │   │   │       │   ├── cicd.json
│   │   │   │   │       │   ├── CLAUDE.md
│   │   │   │   │       │   ├── client.json
│   │   │   │   │       │   ├── cloud.json
│   │   │   │   │       │   ├── cloudevents.json
│   │   │   │   │       │   ├── cloudfoundry.json
│   │   │   │   │       │   ├── code.json
│   │   │   │   │       │   ├── container.json
│   │   │   │   │       │   ├── cpu.json
│   │   │   │   │       │   ├── cpython.json
│   │   │   │   │       │   ├── database.json
│   │   │   │   │       │   ├── db.json
│   │   │   │   │       │   ├── deployment.json
│   │   │   │   │       │   ├── destination.json
│   │   │   │   │       │   ├── device.json
│   │   │   │   │       │   ├── disk.json
│   │   │   │   │       │   ├── dns.json
│   │   │   │   │       │   ├── dotnet.json
│   │   │   │   │       │   ├── elasticsearch.json
│   │   │   │   │       │   ├── enduser.json
│   │   │   │   │       │   ├── error.json
│   │   │   │   │       │   ├── faas.json
│   │   │   │   │       │   ├── feature_flags.json
│   │   │   │   │       │   ├── file.json
│   │   │   │   │       │   ├── gcp.json
│   │   │   │   │       │   ├── gen_ai.json
│   │   │   │   │       │   ├── geo.json
│   │   │   │   │       │   ├── go.json
│   │   │   │   │       │   ├── graphql.json
│   │   │   │   │       │   ├── hardware.json
│   │   │   │   │       │   ├── heroku.json
│   │   │   │   │       │   ├── host.json
│   │   │   │   │       │   ├── http.json
│   │   │   │   │       │   ├── ios.json
│   │   │   │   │       │   ├── jvm.json
│   │   │   │   │       │   ├── k8s.json
│   │   │   │   │       │   ├── linux.json
│   │   │   │   │       │   ├── log.json
│   │   │   │   │       │   ├── mcp.json
│   │   │   │   │       │   ├── messaging.json
│   │   │   │   │       │   ├── network.json
│   │   │   │   │       │   ├── nodejs.json
│   │   │   │   │       │   ├── oci.json
│   │   │   │   │       │   ├── opentracing.json
│   │   │   │   │       │   ├── os.json
│   │   │   │   │       │   ├── otel.json
│   │   │   │   │       │   ├── peer.json
│   │   │   │   │       │   ├── process.json
│   │   │   │   │       │   ├── profile.json
│   │   │   │   │       │   ├── rpc.json
│   │   │   │   │       │   ├── server.json
│   │   │   │   │       │   ├── service.json
│   │   │   │   │       │   ├── session.json
│   │   │   │   │       │   ├── signalr.json
│   │   │   │   │       │   ├── source.json
│   │   │   │   │       │   ├── system.json
│   │   │   │   │       │   ├── telemetry.json
│   │   │   │   │       │   ├── test.json
│   │   │   │   │       │   ├── thread.json
│   │   │   │   │       │   ├── tls.json
│   │   │   │   │       │   ├── url.json
│   │   │   │   │       │   ├── user.json
│   │   │   │   │       │   ├── v8js.json
│   │   │   │   │       │   ├── vcs.json
│   │   │   │   │       │   ├── webengine.json
│   │   │   │   │       │   └── zos.json
│   │   │   │   │       ├── dataset-fields.test.ts
│   │   │   │   │       ├── dataset-fields.ts
│   │   │   │   │       ├── otel-semantics.test.ts
│   │   │   │   │       ├── otel-semantics.ts
│   │   │   │   │       ├── utils.ts
│   │   │   │   │       ├── whoami.test.ts
│   │   │   │   │       └── whoami.ts
│   │   │   │   ├── constraint-helpers.test.ts
│   │   │   │   ├── constraint-helpers.ts
│   │   │   │   ├── error-handling.ts
│   │   │   │   ├── fetch-utils.test.ts
│   │   │   │   ├── fetch-utils.ts
│   │   │   │   ├── formatting.test.ts
│   │   │   │   ├── formatting.ts
│   │   │   │   ├── issue-helpers.test.ts
│   │   │   │   ├── issue-helpers.ts
│   │   │   │   ├── test-fixtures.ts
│   │   │   │   └── tool-helpers
│   │   │   │       ├── api.test.ts
│   │   │   │       ├── api.ts
│   │   │   │       ├── define.ts
│   │   │   │       ├── enhance-error.ts
│   │   │   │       ├── formatting.ts
│   │   │   │       ├── issue.ts
│   │   │   │       ├── seer.test.ts
│   │   │   │       ├── seer.ts
│   │   │   │       ├── validate-region-url.test.ts
│   │   │   │       └── validate-region-url.ts
│   │   │   ├── permissions.parseScopes.test.ts
│   │   │   ├── permissions.ts
│   │   │   ├── schema.ts
│   │   │   ├── server.ts
│   │   │   ├── skillDefinitions.json
│   │   │   ├── skillDefinitions.ts
│   │   │   ├── skills.test.ts
│   │   │   ├── skills.ts
│   │   │   ├── telem
│   │   │   │   ├── index.ts
│   │   │   │   ├── logging.ts
│   │   │   │   ├── sentry.test.ts
│   │   │   │   └── sentry.ts
│   │   │   ├── test-setup.ts
│   │   │   ├── test-utils
│   │   │   │   └── context.ts
│   │   │   ├── toolDefinitions.json
│   │   │   ├── toolDefinitions.ts
│   │   │   ├── tools
│   │   │   │   ├── analyze-issue-with-seer.test.ts
│   │   │   │   ├── analyze-issue-with-seer.ts
│   │   │   │   ├── create-dsn.test.ts
│   │   │   │   ├── create-dsn.ts
│   │   │   │   ├── create-project.test.ts
│   │   │   │   ├── create-project.ts
│   │   │   │   ├── create-team.test.ts
│   │   │   │   ├── create-team.ts
│   │   │   │   ├── find-dsns.test.ts
│   │   │   │   ├── find-dsns.ts
│   │   │   │   ├── find-organizations.test.ts
│   │   │   │   ├── find-organizations.ts
│   │   │   │   ├── find-projects.test.ts
│   │   │   │   ├── find-projects.ts
│   │   │   │   ├── find-releases.test.ts
│   │   │   │   ├── find-releases.ts
│   │   │   │   ├── find-teams.test.ts
│   │   │   │   ├── find-teams.ts
│   │   │   │   ├── get-doc.test.ts
│   │   │   │   ├── get-doc.ts
│   │   │   │   ├── get-event-attachment.test.ts
│   │   │   │   ├── get-event-attachment.ts
│   │   │   │   ├── get-issue-details.test.ts
│   │   │   │   ├── get-issue-details.ts
│   │   │   │   ├── get-trace-details.test.ts
│   │   │   │   ├── get-trace-details.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── search-docs.test.ts
│   │   │   │   ├── search-docs.ts
│   │   │   │   ├── search-events
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── formatters.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── utils.test.ts
│   │   │   │   │   └── utils.ts
│   │   │   │   ├── search-events.test.ts
│   │   │   │   ├── search-issues
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── formatters.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── README.md
│   │   │   │   ├── tools.test.ts
│   │   │   │   ├── types.ts
│   │   │   │   ├── update-issue.test.ts
│   │   │   │   ├── update-issue.ts
│   │   │   │   ├── update-project.test.ts
│   │   │   │   ├── update-project.ts
│   │   │   │   ├── use-sentry
│   │   │   │   │   ├── agent.ts
│   │   │   │   │   ├── CLAUDE.md
│   │   │   │   │   ├── config.ts
│   │   │   │   │   ├── handler.test.ts
│   │   │   │   │   ├── handler.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── tool-wrapper.test.ts
│   │   │   │   │   └── tool-wrapper.ts
│   │   │   │   ├── whoami.test.ts
│   │   │   │   └── whoami.ts
│   │   │   ├── types.ts
│   │   │   ├── utils
│   │   │   │   ├── slug-validation.test.ts
│   │   │   │   ├── slug-validation.ts
│   │   │   │   ├── url-utils.test.ts
│   │   │   │   └── url-utils.ts
│   │   │   └── version.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   ├── mcp-server
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── cli
│   │   │   │   ├── parse.test.ts
│   │   │   │   ├── parse.ts
│   │   │   │   ├── resolve.test.ts
│   │   │   │   ├── resolve.ts
│   │   │   │   ├── types.ts
│   │   │   │   └── usage.ts
│   │   │   ├── index.ts
│   │   │   └── transports
│   │   │       └── stdio.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── mcp-server-evals
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── bin
│   │   │   │   └── start-mock-stdio.ts
│   │   │   ├── evals
│   │   │   │   ├── autofix.eval.ts
│   │   │   │   ├── create-dsn.eval.ts
│   │   │   │   ├── create-project.eval.ts
│   │   │   │   ├── create-team.eval.ts
│   │   │   │   ├── get-issue.eval.ts
│   │   │   │   ├── get-trace-details.eval.ts
│   │   │   │   ├── list-dsns.eval.ts
│   │   │   │   ├── list-issues.eval.ts
│   │   │   │   ├── list-organizations.eval.ts
│   │   │   │   ├── list-projects.eval.ts
│   │   │   │   ├── list-releases.eval.ts
│   │   │   │   ├── list-tags.eval.ts
│   │   │   │   ├── list-teams.eval.ts
│   │   │   │   ├── search-docs.eval.ts
│   │   │   │   ├── search-events-agent.eval.ts
│   │   │   │   ├── search-events.eval.ts
│   │   │   │   ├── search-issues-agent.eval.ts
│   │   │   │   ├── search-issues.eval.ts
│   │   │   │   ├── update-issue.eval.ts
│   │   │   │   ├── update-project.eval.ts
│   │   │   │   └── utils
│   │   │   │       ├── fixtures.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── runner.ts
│   │   │   │       ├── structuredOutputScorer.ts
│   │   │   │       └── toolPredictionScorer.ts
│   │   │   └── setup-env.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── mcp-server-mocks
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── fixtures
│   │   │   │   ├── autofix-state.json
│   │   │   │   ├── csp-event.json
│   │   │   │   ├── csp-issue.json
│   │   │   │   ├── default-event.json
│   │   │   │   ├── event-attachments.json
│   │   │   │   ├── event.json
│   │   │   │   ├── generic-event.json
│   │   │   │   ├── issue.json
│   │   │   │   ├── performance-event.json
│   │   │   │   ├── performance-issue.json
│   │   │   │   ├── project.json
│   │   │   │   ├── regressed-issue.json
│   │   │   │   ├── tags.json
│   │   │   │   ├── team.json
│   │   │   │   ├── trace-event.json
│   │   │   │   ├── trace-items-attributes-logs-number.json
│   │   │   │   ├── trace-items-attributes-logs-string.json
│   │   │   │   ├── trace-items-attributes-spans-number.json
│   │   │   │   ├── trace-items-attributes-spans-string.json
│   │   │   │   ├── trace-items-attributes.json
│   │   │   │   ├── trace-meta-with-nulls.json
│   │   │   │   ├── trace-meta.json
│   │   │   │   ├── trace-mixed.json
│   │   │   │   ├── trace.json
│   │   │   │   ├── unknown-event.json
│   │   │   │   └── unsupported-issue.json
│   │   │   ├── fixtures.ts
│   │   │   ├── index.ts
│   │   │   └── utils.ts
│   │   ├── tsconfig.json
│   │   └── tsdown.config.ts
│   ├── mcp-server-tsconfig
│   │   ├── package.json
│   │   ├── tsconfig.base.json
│   │   └── tsconfig.vite.json
│   ├── mcp-test-client
│   │   ├── .env.test
│   │   ├── .gitignore
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── agent.ts
│   │   │   ├── auth
│   │   │   │   ├── config.ts
│   │   │   │   └── oauth.ts
│   │   │   ├── constants.ts
│   │   │   ├── index.ts
│   │   │   ├── logger.test.ts
│   │   │   ├── logger.ts
│   │   │   ├── mcp-test-client-remote.ts
│   │   │   ├── mcp-test-client.ts
│   │   │   ├── types.ts
│   │   │   └── version.ts
│   │   ├── tsconfig.json
│   │   ├── tsdown.config.ts
│   │   └── vitest.config.ts
│   └── smoke-tests
│       ├── package.json
│       ├── src
│       │   └── smoke.test.ts
│       └── vitest.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── README.md
├── scripts
│   └── check-doc-links.mjs
├── turbo.json
└── vitest.workspace.ts
```

# Files

--------------------------------------------------------------------------------
/packages/mcp-core/src/internal/formatting.test.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { describe, it, expect } from "vitest";
   2 | import { formatEventOutput, formatFrameHeader } from "./formatting";
   3 | import type { Event } from "../api-client/types";
   4 | import {
   5 |   EventBuilder,
   6 |   createFrame,
   7 |   frameFactories,
   8 |   createStackTrace,
   9 |   createExceptionValue,
  10 |   createThread,
  11 |   testEvents,
  12 |   createFrameWithContext,
  13 | } from "./test-fixtures";
  14 | 
  15 | // Helper functions to reduce duplication in event creation
  16 | function createPythonExceptionEvent(
  17 |   errorType: string,
  18 |   errorMessage: string,
  19 |   frames: any[],
  20 | ): Event {
  21 |   return new EventBuilder("python")
  22 |     .withException(
  23 |       createExceptionValue({
  24 |         type: errorType,
  25 |         value: errorMessage,
  26 |         stacktrace: createStackTrace(frames),
  27 |       }),
  28 |     )
  29 |     .build();
  30 | }
  31 | 
  32 | function createSimpleExceptionEvent(
  33 |   platform: string,
  34 |   errorType: string,
  35 |   errorMessage: string,
  36 |   frame: any,
  37 | ): Event {
  38 |   const builder = new EventBuilder(platform);
  39 |   // Remove the contexts property to avoid "Additional Context" section
  40 |   const event = builder
  41 |     .withException(
  42 |       createExceptionValue({
  43 |         type: errorType,
  44 |         value: errorMessage,
  45 |         stacktrace: createStackTrace([frame]),
  46 |       }),
  47 |     )
  48 |     .build();
  49 |   // Remove contexts to match original test expectations
  50 |   event.contexts = undefined;
  51 |   return event;
  52 | }
  53 | 
  54 | describe("formatFrameHeader", () => {
  55 |   it("uses platform as fallback when language detection fails", () => {
  56 |     // Frame with no clear language indicators
  57 |     const unknownFrame = {
  58 |       filename: "/path/to/file.unknown",
  59 |       function: "someFunction",
  60 |       lineNo: 42,
  61 |     };
  62 | 
  63 |     // Without platform - should use generic format
  64 |     expect(formatFrameHeader(unknownFrame)).toBe(
  65 |       "    at someFunction (/path/to/file.unknown:42)",
  66 |     );
  67 | 
  68 |     // With platform python - should use Python format
  69 |     expect(formatFrameHeader(unknownFrame, undefined, "python")).toBe(
  70 |       '  File "/path/to/file.unknown", line 42, in someFunction',
  71 |     );
  72 | 
  73 |     // With platform java - should use Java format
  74 |     expect(formatFrameHeader(unknownFrame, undefined, "java")).toBe(
  75 |       "at UnknownClass.someFunction(/path/to/file.unknown:42)",
  76 |     );
  77 |   });
  78 |   it("formats Java stack traces correctly", () => {
  79 |     // With module and filename
  80 |     const javaFrame1 = {
  81 |       module: "com.example.ClassName",
  82 |       function: "methodName",
  83 |       filename: "ClassName.java",
  84 |       lineNo: 123,
  85 |     };
  86 |     expect(formatFrameHeader(javaFrame1)).toBe(
  87 |       "at com.example.ClassName.methodName(ClassName.java:123)",
  88 |     );
  89 | 
  90 |     // Without filename (common in Java) - needs platform hint
  91 |     const javaFrame2 = {
  92 |       module: "com.example.ClassName",
  93 |       function: "methodName",
  94 |       lineNo: 123,
  95 |     };
  96 |     expect(formatFrameHeader(javaFrame2, undefined, "java")).toBe(
  97 |       "at com.example.ClassName.methodName(Unknown Source:123)",
  98 |     );
  99 |   });
 100 | 
 101 |   it("formats Python stack traces correctly", () => {
 102 |     const pythonFrame = {
 103 |       filename: "/path/to/file.py",
 104 |       function: "function_name",
 105 |       lineNo: 42,
 106 |     };
 107 |     expect(formatFrameHeader(pythonFrame)).toBe(
 108 |       '  File "/path/to/file.py", line 42, in function_name',
 109 |     );
 110 | 
 111 |     // Module only (no filename) - needs platform hint
 112 |     const pythonModuleFrame = {
 113 |       module: "mymodule",
 114 |       function: "function_name",
 115 |       lineNo: 42,
 116 |     };
 117 |     expect(formatFrameHeader(pythonModuleFrame, undefined, "python")).toBe(
 118 |       '  File "mymodule", line 42, in function_name',
 119 |     );
 120 |   });
 121 | 
 122 |   it("formats JavaScript stack traces correctly", () => {
 123 |     // With column number
 124 |     const jsFrame1 = {
 125 |       filename: "/path/to/file.js",
 126 |       function: "functionName",
 127 |       lineNo: 10,
 128 |       colNo: 15,
 129 |     };
 130 |     expect(formatFrameHeader(jsFrame1)).toBe(
 131 |       "/path/to/file.js:10:15 (functionName)",
 132 |     );
 133 | 
 134 |     // Without column number but .js extension
 135 |     const jsFrame2 = {
 136 |       filename: "/path/to/file.js",
 137 |       function: "functionName",
 138 |       lineNo: 10,
 139 |     };
 140 |     expect(formatFrameHeader(jsFrame2)).toBe(
 141 |       "/path/to/file.js:10 (functionName)",
 142 |     );
 143 | 
 144 |     // Anonymous function (no function name)
 145 |     const jsFrame3 = {
 146 |       filename: "/path/to/file.js",
 147 |       lineNo: 10,
 148 |       colNo: 15,
 149 |     };
 150 |     expect(formatFrameHeader(jsFrame3)).toBe("/path/to/file.js:10:15");
 151 |   });
 152 | 
 153 |   it("formats Ruby stack traces correctly", () => {
 154 |     const rubyFrame = {
 155 |       filename: "/path/to/file.rb",
 156 |       function: "method_name",
 157 |       lineNo: 42,
 158 |     };
 159 |     expect(formatFrameHeader(rubyFrame)).toBe(
 160 |       "    from /path/to/file.rb:42:in `method_name`",
 161 |     );
 162 | 
 163 |     // Without function name
 164 |     const rubyFrame2 = {
 165 |       filename: "/path/to/file.rb",
 166 |       lineNo: 42,
 167 |     };
 168 |     expect(formatFrameHeader(rubyFrame2)).toBe(
 169 |       "    from /path/to/file.rb:42:in",
 170 |     );
 171 |   });
 172 | 
 173 |   it("formats PHP stack traces correctly", () => {
 174 |     // With frame index
 175 |     const phpFrame1 = {
 176 |       filename: "/path/to/file.php",
 177 |       function: "functionName",
 178 |       lineNo: 42,
 179 |     };
 180 |     expect(formatFrameHeader(phpFrame1, 0)).toBe(
 181 |       "#0 /path/to/file.php(42): functionName()",
 182 |     );
 183 | 
 184 |     // Without frame index
 185 |     const phpFrame2 = {
 186 |       filename: "/path/to/file.php",
 187 |       function: "functionName",
 188 |       lineNo: 42,
 189 |     };
 190 |     expect(formatFrameHeader(phpFrame2)).toBe(
 191 |       "/path/to/file.php(42): functionName()",
 192 |     );
 193 |   });
 194 | 
 195 |   it("formats unknown languages with generic format", () => {
 196 |     const unknownFrame = {
 197 |       filename: "/path/to/file.unknown",
 198 |       function: "someFunction",
 199 |       lineNo: 42,
 200 |     };
 201 |     expect(formatFrameHeader(unknownFrame)).toBe(
 202 |       "    at someFunction (/path/to/file.unknown:42)",
 203 |     );
 204 |   });
 205 | 
 206 |   it("prioritizes duck typing over platform when clear indicators exist", () => {
 207 |     // Java file but platform says python - should use Java format
 208 |     const javaFrame = {
 209 |       filename: "Example.java",
 210 |       module: "com.example.Example",
 211 |       function: "doSomething",
 212 |       lineNo: 42,
 213 |     };
 214 |     expect(formatFrameHeader(javaFrame, undefined, "python")).toBe(
 215 |       "at com.example.Example.doSomething(Example.java:42)",
 216 |     );
 217 | 
 218 |     // Python file but platform says java - should use Python format
 219 |     const pythonFrame = {
 220 |       filename: "/app/example.py",
 221 |       function: "do_something",
 222 |       lineNo: 42,
 223 |     };
 224 |     expect(formatFrameHeader(pythonFrame, undefined, "java")).toBe(
 225 |       '  File "/app/example.py", line 42, in do_something',
 226 |     );
 227 |   });
 228 | });
 229 | 
 230 | describe("formatEventOutput", () => {
 231 |   it("formats Java thread stack traces correctly", () => {
 232 |     const event = testEvents.javaThreadError(
 233 |       "Cannot use this function, please use update(String sql, PreparedStatementSetter pss) instead",
 234 |     );
 235 | 
 236 |     const output = formatEventOutput(event);
 237 | 
 238 |     expect(output).toMatchInlineSnapshot(`
 239 |       "### Error
 240 | 
 241 |       \`\`\`
 242 |       Cannot use this function, please use update(String sql, PreparedStatementSetter pss) instead
 243 |       \`\`\`
 244 | 
 245 |       **Thread** (CONTRACT_WORKER)
 246 | 
 247 |       **Stacktrace:**
 248 |       \`\`\`
 249 |       at java.lang.Thread.run(Thread.java:833)
 250 |       at com.citics.eqd.mq.aeron.AeronServer.lambda$start$3(AeronServer.java:110)
 251 |       \`\`\`
 252 |       "
 253 |     `);
 254 |   });
 255 | 
 256 |   it("formats Python exception traces correctly", () => {
 257 |     const event = testEvents.pythonException("Invalid value");
 258 | 
 259 |     const output = formatEventOutput(event);
 260 | 
 261 |     expect(output).toMatchInlineSnapshot(`
 262 |       "### Error
 263 | 
 264 |       \`\`\`
 265 |       ValueError: Invalid value
 266 |       \`\`\`
 267 | 
 268 |       **Stacktrace:**
 269 |       \`\`\`
 270 |         File "/app/main.py", line 42, in process_data
 271 |         File "/app/utils.py", line 15, in validate
 272 |       \`\`\`
 273 | 
 274 |       "
 275 |     `);
 276 |   });
 277 | 
 278 |   it("should render enhanced in-app frame with context lines", () => {
 279 |     const event = new EventBuilder("python")
 280 |       .withException(
 281 |         createExceptionValue({
 282 |           type: "ValueError",
 283 |           value: "Something went wrong",
 284 |           stacktrace: createStackTrace([
 285 |             createFrame({
 286 |               filename: "/usr/lib/python3.8/json/__init__.py",
 287 |               function: "loads",
 288 |               lineNo: 357,
 289 |               inApp: false,
 290 |             }),
 291 |             createFrameWithContext(
 292 |               {
 293 |                 filename: "/app/services/payment.py",
 294 |                 function: "process_payment",
 295 |                 lineNo: 42,
 296 |               },
 297 |               [
 298 |                 [37, "    def process_payment(self, amount, user_id):"],
 299 |                 [38, "        user = self.get_user(user_id)"],
 300 |                 [39, "        if not user:"],
 301 |                 [40, '            raise ValueError("User not found")'],
 302 |                 [41, "        "],
 303 |                 [42, "        balance = user.account.balance"],
 304 |                 [43, "        if balance < amount:"],
 305 |                 [44, "            raise InsufficientFundsError()"],
 306 |                 [45, "        "],
 307 |                 [46, "        transaction = Transaction(user, amount)"],
 308 |               ],
 309 |             ),
 310 |           ]),
 311 |         }),
 312 |       )
 313 |       .build();
 314 | 
 315 |     const output = formatEventOutput(event);
 316 | 
 317 |     expect(output).toMatchInlineSnapshot(`
 318 |       "### Error
 319 | 
 320 |       \`\`\`
 321 |       ValueError: Something went wrong
 322 |       \`\`\`
 323 | 
 324 |       **Most Relevant Frame:**
 325 |       ─────────────────────
 326 |         File "/app/services/payment.py", line 42, in process_payment
 327 | 
 328 |           39 │         if not user:
 329 |           40 │             raise ValueError("User not found")
 330 |           41 │         
 331 |         → 42 │         balance = user.account.balance
 332 |           43 │         if balance < amount:
 333 |           44 │             raise InsufficientFundsError()
 334 |           45 │         
 335 | 
 336 |       **Full Stacktrace:**
 337 |       ────────────────
 338 |       \`\`\`
 339 |         File "/usr/lib/python3.8/json/__init__.py", line 357, in loads
 340 |         File "/app/services/payment.py", line 42, in process_payment
 341 |               balance = user.account.balance
 342 |       \`\`\`
 343 | 
 344 |       "
 345 |     `);
 346 |   });
 347 | 
 348 |   it("should render enhanced in-app frame with variables", () => {
 349 |     const event = new EventBuilder("python")
 350 |       .withException(
 351 |         createExceptionValue({
 352 |           type: "ValueError",
 353 |           value: "Something went wrong",
 354 |           stacktrace: createStackTrace([
 355 |             createFrame({
 356 |               filename: "/app/services/payment.py",
 357 |               function: "process_payment",
 358 |               lineNo: 42,
 359 |               inApp: true,
 360 |               vars: {
 361 |                 amount: 150.0,
 362 |                 user_id: "usr_123456",
 363 |                 user: null,
 364 |                 self: { type: "PaymentService", id: 1234 },
 365 |               },
 366 |             }),
 367 |           ]),
 368 |         }),
 369 |       )
 370 |       .build();
 371 | 
 372 |     const output = formatEventOutput(event);
 373 | 
 374 |     expect(output).toMatchInlineSnapshot(`
 375 |       "### Error
 376 | 
 377 |       \`\`\`
 378 |       ValueError: Something went wrong
 379 |       \`\`\`
 380 | 
 381 |       **Most Relevant Frame:**
 382 |       ─────────────────────
 383 |         File "/app/services/payment.py", line 42, in process_payment
 384 | 
 385 |       Local Variables:
 386 |       ├─ amount: 150
 387 |       ├─ user_id: "usr_123456"
 388 |       ├─ user: null
 389 |       └─ self: {"type":"PaymentService","id":1234}
 390 | 
 391 |       **Full Stacktrace:**
 392 |       ────────────────
 393 |       \`\`\`
 394 |         File "/app/services/payment.py", line 42, in process_payment
 395 |       \`\`\`
 396 | 
 397 |       "
 398 |     `);
 399 |   });
 400 | 
 401 |   it("should handle frames without in-app or enhanced data", () => {
 402 |     const event = new EventBuilder("python")
 403 |       .withException(
 404 |         createExceptionValue({
 405 |           type: "ValueError",
 406 |           value: "Something went wrong",
 407 |           stacktrace: createStackTrace([
 408 |             frameFactories.python({ lineNo: 10, function: "main" }),
 409 |           ]),
 410 |         }),
 411 |       )
 412 |       .build();
 413 | 
 414 |     const output = formatEventOutput(event);
 415 | 
 416 |     expect(output).toMatchInlineSnapshot(`
 417 |       "### Error
 418 | 
 419 |       \`\`\`
 420 |       ValueError: Something went wrong
 421 |       \`\`\`
 422 | 
 423 |       **Stacktrace:**
 424 |       \`\`\`
 425 |         File "/app/main.py", line 10, in main
 426 |       \`\`\`
 427 | 
 428 |       "
 429 |     `);
 430 |   });
 431 | 
 432 |   it("should work with thread interface containing in-app frame", () => {
 433 |     const event = new EventBuilder("java")
 434 |       .withThread(
 435 |         createThread({
 436 |           id: 1,
 437 |           crashed: true,
 438 |           name: "main",
 439 |           stacktrace: createStackTrace([
 440 |             frameFactories.java({
 441 |               module: "java.lang.Thread",
 442 |               function: "run",
 443 |               filename: "Thread.java",
 444 |               lineNo: 748,
 445 |               inApp: false,
 446 |             }),
 447 |             createFrameWithContext(
 448 |               {
 449 |                 module: "com.example.PaymentService",
 450 |                 function: "processPayment",
 451 |                 filename: "PaymentService.java",
 452 |                 lineNo: 42,
 453 |               },
 454 |               [
 455 |                 [40, "        User user = getUser(userId);"],
 456 |                 [41, "        if (user == null) {"],
 457 |                 [42, "            throw new UserNotFoundException(userId);"],
 458 |                 [43, "        }"],
 459 |                 [44, "        return user.getBalance();"],
 460 |               ],
 461 |               {
 462 |                 userId: "12345",
 463 |                 user: null,
 464 |               },
 465 |             ),
 466 |           ]),
 467 |         }),
 468 |       )
 469 |       .build();
 470 | 
 471 |     const output = formatEventOutput(event);
 472 | 
 473 |     expect(output).toMatchInlineSnapshot(`
 474 |       "**Thread** (main)
 475 | 
 476 |       **Most Relevant Frame:**
 477 |       ─────────────────────
 478 |       at com.example.PaymentService.processPayment(PaymentService.java:42)
 479 | 
 480 |           40 │         User user = getUser(userId);
 481 |           41 │         if (user == null) {
 482 |         → 42 │             throw new UserNotFoundException(userId);
 483 |           43 │         }
 484 |           44 │         return user.getBalance();
 485 | 
 486 |       Local Variables:
 487 |       ├─ userId: "12345"
 488 |       └─ user: null
 489 | 
 490 |       **Full Stacktrace:**
 491 |       ────────────────
 492 |       \`\`\`
 493 |       at java.lang.Thread.run(Thread.java:748)
 494 |       at com.example.PaymentService.processPayment(PaymentService.java:42)
 495 |                   throw new UserNotFoundException(userId);
 496 |       \`\`\`
 497 |       "
 498 |     `);
 499 |   });
 500 | 
 501 |   describe("Enhanced frame rendering variations", () => {
 502 |     it("should handle Python format with enhanced frame", () => {
 503 |       const event = createSimpleExceptionEvent(
 504 |         "python",
 505 |         "AttributeError",
 506 |         "'NoneType' object has no attribute 'balance'",
 507 |         createFrameWithContext(
 508 |           {
 509 |             filename: "/app/models/user.py",
 510 |             function: "get_balance",
 511 |             lineNo: 25,
 512 |           },
 513 |           [
 514 |             [23, "    def get_balance(self):"],
 515 |             [24, "        # This will fail if account is None"],
 516 |             [25, "        return self.account.balance"],
 517 |             [26, ""],
 518 |             [27, "    def set_balance(self, amount):"],
 519 |           ],
 520 |           {
 521 |             self: { id: 123, account: null },
 522 |           },
 523 |         ),
 524 |       );
 525 | 
 526 |       const output = formatEventOutput(event);
 527 | 
 528 |       expect(output).toMatchInlineSnapshot(`
 529 |         "### Error
 530 | 
 531 |         \`\`\`
 532 |         AttributeError: 'NoneType' object has no attribute 'balance'
 533 |         \`\`\`
 534 | 
 535 |         **Most Relevant Frame:**
 536 |         ─────────────────────
 537 |           File "/app/models/user.py", line 25, in get_balance
 538 | 
 539 |             23 │     def get_balance(self):
 540 |             24 │         # This will fail if account is None
 541 |           → 25 │         return self.account.balance
 542 |             26 │ 
 543 |             27 │     def set_balance(self, amount):
 544 | 
 545 |         Local Variables:
 546 |         └─ self: {"id":123,"account":null}
 547 | 
 548 |         **Full Stacktrace:**
 549 |         ────────────────
 550 |         \`\`\`
 551 |           File "/app/models/user.py", line 25, in get_balance
 552 |                 return self.account.balance
 553 |         \`\`\`
 554 | 
 555 |         "
 556 |       `);
 557 |     });
 558 | 
 559 |     it("should handle JavaScript format with enhanced frame", () => {
 560 |       const event = createSimpleExceptionEvent(
 561 |         "javascript",
 562 |         "TypeError",
 563 |         "Cannot read property 'name' of undefined",
 564 |         createFrameWithContext(
 565 |           {
 566 |             filename: "/src/components/UserProfile.tsx",
 567 |             function: "UserProfile",
 568 |             lineNo: 15,
 569 |             colNo: 28,
 570 |           },
 571 |           [
 572 |             [
 573 |               13,
 574 |               "export const UserProfile: React.FC<Props> = ({ userId }) => {",
 575 |             ],
 576 |             [14, "  const user = useUser(userId);"],
 577 |             [15, "  const displayName = user.profile.name;"],
 578 |             [16, "  "],
 579 |             [17, "  return ("],
 580 |           ],
 581 |           {
 582 |             userId: "usr_123",
 583 |             user: undefined,
 584 |             displayName: undefined,
 585 |           },
 586 |         ),
 587 |       );
 588 | 
 589 |       const output = formatEventOutput(event);
 590 | 
 591 |       expect(output).toMatchInlineSnapshot(`
 592 |         "### Error
 593 | 
 594 |         \`\`\`
 595 |         TypeError: Cannot read property 'name' of undefined
 596 |         \`\`\`
 597 | 
 598 |         **Most Relevant Frame:**
 599 |         ─────────────────────
 600 |         /src/components/UserProfile.tsx:15:28 (UserProfile)
 601 | 
 602 |             13 │ export const UserProfile: React.FC<Props> = ({ userId }) => {
 603 |             14 │   const user = useUser(userId);
 604 |           → 15 │   const displayName = user.profile.name;
 605 |             16 │   
 606 |             17 │   return (
 607 | 
 608 |         Local Variables:
 609 |         ├─ userId: "usr_123"
 610 |         ├─ user: undefined
 611 |         └─ displayName: undefined
 612 | 
 613 |         **Full Stacktrace:**
 614 |         ────────────────
 615 |         \`\`\`
 616 |         /src/components/UserProfile.tsx:15:28 (UserProfile)
 617 |           const displayName = user.profile.name;
 618 |         \`\`\`
 619 | 
 620 |         "
 621 |       `);
 622 |     });
 623 | 
 624 |     it("should handle Ruby format with enhanced frame", () => {
 625 |       const event = new EventBuilder("ruby")
 626 |         .withException(
 627 |           createExceptionValue({
 628 |             type: "NoMethodError",
 629 |             value: "undefined method `charge' for nil:NilClass",
 630 |             stacktrace: createStackTrace([
 631 |               createFrameWithContext(
 632 |                 {
 633 |                   filename: "/app/services/payment_service.rb",
 634 |                   function: "process_payment",
 635 |                   lineNo: 8,
 636 |                 },
 637 |                 [
 638 |                   [6, "  def process_payment(amount)"],
 639 |                   [7, "    payment_method = user.payment_method"],
 640 |                   [8, "    payment_method.charge(amount)"],
 641 |                   [9, "  rescue => e"],
 642 |                   [10, "    Rails.logger.error(e)"],
 643 |                 ],
 644 |                 {
 645 |                   amount: 99.99,
 646 |                   payment_method: null,
 647 |                 },
 648 |               ),
 649 |             ]),
 650 |           }),
 651 |         )
 652 |         .build();
 653 | 
 654 |       const output = formatEventOutput(event);
 655 | 
 656 |       expect(output).toMatchInlineSnapshot(`
 657 |         "### Error
 658 | 
 659 |         \`\`\`
 660 |         NoMethodError: undefined method \`charge' for nil:NilClass
 661 |         \`\`\`
 662 | 
 663 |         **Most Relevant Frame:**
 664 |         ─────────────────────
 665 |             from /app/services/payment_service.rb:8:in \`process_payment\`
 666 | 
 667 |              6 │   def process_payment(amount)
 668 |              7 │     payment_method = user.payment_method
 669 |           →  8 │     payment_method.charge(amount)
 670 |              9 │   rescue => e
 671 |             10 │     Rails.logger.error(e)
 672 | 
 673 |         Local Variables:
 674 |         ├─ amount: 99.99
 675 |         └─ payment_method: null
 676 | 
 677 |         **Full Stacktrace:**
 678 |         ────────────────
 679 |         \`\`\`
 680 |             from /app/services/payment_service.rb:8:in \`process_payment\`
 681 |             payment_method.charge(amount)
 682 |         \`\`\`
 683 | 
 684 |         "
 685 |       `);
 686 |     });
 687 | 
 688 |     it("should handle PHP format with enhanced frame", () => {
 689 |       const event = new EventBuilder("php")
 690 |         .withException(
 691 |           createExceptionValue({
 692 |             type: "Error",
 693 |             value: "Call to a member function getName() on null",
 694 |             stacktrace: createStackTrace([
 695 |               createFrameWithContext(
 696 |                 {
 697 |                   filename: "/var/www/app/User.php",
 698 |                   function: "getDisplayName",
 699 |                   lineNo: 45,
 700 |                 },
 701 |                 [
 702 |                   [43, "    public function getDisplayName() {"],
 703 |                   [44, "        $profile = $this->getProfile();"],
 704 |                   [45, "        return $profile->getName();"],
 705 |                   [46, "    }"],
 706 |                 ],
 707 |                 {
 708 |                   profile: null,
 709 |                 },
 710 |               ),
 711 |             ]),
 712 |           }),
 713 |         )
 714 |         .build();
 715 | 
 716 |       const output = formatEventOutput(event);
 717 | 
 718 |       expect(output).toMatchInlineSnapshot(`
 719 |         "### Error
 720 | 
 721 |         \`\`\`
 722 |         Error: Call to a member function getName() on null
 723 |         \`\`\`
 724 | 
 725 |         **Most Relevant Frame:**
 726 |         ─────────────────────
 727 |         /var/www/app/User.php(45): getDisplayName()
 728 | 
 729 |             43 │     public function getDisplayName() {
 730 |             44 │         $profile = $this->getProfile();
 731 |           → 45 │         return $profile->getName();
 732 |             46 │     }
 733 | 
 734 |         Local Variables:
 735 |         └─ profile: null
 736 | 
 737 |         **Full Stacktrace:**
 738 |         ────────────────
 739 |         \`\`\`
 740 |         /var/www/app/User.php(45): getDisplayName()
 741 |                 return $profile->getName();
 742 |         \`\`\`
 743 | 
 744 |         "
 745 |       `);
 746 |     });
 747 | 
 748 |     it("should handle frame with context but no vars", () => {
 749 |       const event = new EventBuilder("python")
 750 |         .withException(
 751 |           createExceptionValue({
 752 |             type: "ValueError",
 753 |             value: "Invalid configuration",
 754 |             stacktrace: createStackTrace([
 755 |               createFrameWithContext(
 756 |                 {
 757 |                   filename: "/app/config.py",
 758 |                   function: "load_config",
 759 |                   lineNo: 12,
 760 |                 },
 761 |                 [
 762 |                   [10, "def load_config():"],
 763 |                   [11, "    if not os.path.exists(CONFIG_FILE):"],
 764 |                   [12, "        raise ValueError('Invalid configuration')"],
 765 |                   [13, "    with open(CONFIG_FILE) as f:"],
 766 |                   [14, "        return json.load(f)"],
 767 |                 ],
 768 |               ),
 769 |             ]),
 770 |           }),
 771 |         )
 772 |         .build();
 773 | 
 774 |       const output = formatEventOutput(event);
 775 | 
 776 |       expect(output).toMatchInlineSnapshot(`
 777 |         "### Error
 778 | 
 779 |         \`\`\`
 780 |         ValueError: Invalid configuration
 781 |         \`\`\`
 782 | 
 783 |         **Most Relevant Frame:**
 784 |         ─────────────────────
 785 |           File "/app/config.py", line 12, in load_config
 786 | 
 787 |             10 │ def load_config():
 788 |             11 │     if not os.path.exists(CONFIG_FILE):
 789 |           → 12 │         raise ValueError('Invalid configuration')
 790 |             13 │     with open(CONFIG_FILE) as f:
 791 |             14 │         return json.load(f)
 792 | 
 793 |         **Full Stacktrace:**
 794 |         ────────────────
 795 |         \`\`\`
 796 |           File "/app/config.py", line 12, in load_config
 797 |                 raise ValueError('Invalid configuration')
 798 |         \`\`\`
 799 | 
 800 |         "
 801 |       `);
 802 |     });
 803 | 
 804 |     it("should handle frame with vars but no context", () => {
 805 |       const event = createSimpleExceptionEvent(
 806 |         "python",
 807 |         "TypeError",
 808 |         "unsupported operand type(s)",
 809 |         createFrame({
 810 |           filename: "/app/calculator.py",
 811 |           function: "divide",
 812 |           lineNo: 5,
 813 |           inApp: true,
 814 |           vars: {
 815 |             numerator: 10,
 816 |             denominator: "0",
 817 |             result: undefined,
 818 |           },
 819 |         }),
 820 |       );
 821 | 
 822 |       const output = formatEventOutput(event);
 823 | 
 824 |       expect(output).toMatchInlineSnapshot(`
 825 |         "### Error
 826 | 
 827 |         \`\`\`
 828 |         TypeError: unsupported operand type(s)
 829 |         \`\`\`
 830 | 
 831 |         **Most Relevant Frame:**
 832 |         ─────────────────────
 833 |           File "/app/calculator.py", line 5, in divide
 834 | 
 835 |         Local Variables:
 836 |         ├─ numerator: 10
 837 |         ├─ denominator: "0"
 838 |         └─ result: undefined
 839 | 
 840 |         **Full Stacktrace:**
 841 |         ────────────────
 842 |         \`\`\`
 843 |           File "/app/calculator.py", line 5, in divide
 844 |         \`\`\`
 845 | 
 846 |         "
 847 |       `);
 848 |     });
 849 | 
 850 |     it("should handle complex variable types", () => {
 851 |       const event = createSimpleExceptionEvent(
 852 |         "python",
 853 |         "KeyError",
 854 |         "'missing_key'",
 855 |         createFrame({
 856 |           filename: "/app/processor.py",
 857 |           function: "process_data",
 858 |           lineNo: 30,
 859 |           inApp: true,
 860 |           vars: {
 861 |             string_var: "hello world",
 862 |             number_var: 42,
 863 |             float_var: 3.14,
 864 |             bool_var: true,
 865 |             null_var: null,
 866 |             undefined_var: undefined,
 867 |             array_var: [1, 2, 3],
 868 |             object_var: { type: "User", id: 123 },
 869 |             nested_object: {
 870 |               user: { name: "John", age: 30 },
 871 |               settings: { theme: "dark" },
 872 |             },
 873 |             empty_string: "",
 874 |             zero: 0,
 875 |             false_bool: false,
 876 |             long_string:
 877 |               "This is a very long string that should be handled properly in the output",
 878 |           },
 879 |         }),
 880 |       );
 881 | 
 882 |       const output = formatEventOutput(event);
 883 | 
 884 |       expect(output).toMatchInlineSnapshot(`
 885 |         "### Error
 886 | 
 887 |         \`\`\`
 888 |         KeyError: 'missing_key'
 889 |         \`\`\`
 890 | 
 891 |         **Most Relevant Frame:**
 892 |         ─────────────────────
 893 |           File "/app/processor.py", line 30, in process_data
 894 | 
 895 |         Local Variables:
 896 |         ├─ string_var: "hello world"
 897 |         ├─ number_var: 42
 898 |         ├─ float_var: 3.14
 899 |         ├─ bool_var: true
 900 |         ├─ null_var: null
 901 |         ├─ undefined_var: undefined
 902 |         ├─ array_var: [1,2,3]
 903 |         ├─ object_var: {"type":"User","id":123}
 904 |         ├─ nested_object: {"user":{"name":"John","age":30},"settings":{"theme":"dark"}}
 905 |         ├─ empty_string: ""
 906 |         ├─ zero: 0
 907 |         ├─ false_bool: false
 908 |         └─ long_string: "This is a very long string that should be handled properly in the output"
 909 | 
 910 |         **Full Stacktrace:**
 911 |         ────────────────
 912 |         \`\`\`
 913 |           File "/app/processor.py", line 30, in process_data
 914 |         \`\`\`
 915 | 
 916 |         "
 917 |       `);
 918 |     });
 919 | 
 920 |     it("should truncate very long objects and arrays", () => {
 921 |       const event = new EventBuilder("python")
 922 |         .withException(
 923 |           createExceptionValue({
 924 |             type: "ValueError",
 925 |             value: "Data processing error",
 926 |             stacktrace: createStackTrace([
 927 |               createFrame({
 928 |                 filename: "/app/processor.py",
 929 |                 function: "process_batch",
 930 |                 lineNo: 45,
 931 |                 inApp: true,
 932 |                 vars: {
 933 |                   small_array: [1, 2, 3],
 934 |                   large_array: Array(100)
 935 |                     .fill(0)
 936 |                     .map((_, i) => i),
 937 |                   small_object: { name: "test", value: 123 },
 938 |                   large_object: {
 939 |                     data: Array(50)
 940 |                       .fill(0)
 941 |                       .reduce(
 942 |                         (acc, _, i) => {
 943 |                           acc[`field${i}`] = `value${i}`;
 944 |                           return acc;
 945 |                         },
 946 |                         {} as Record<string, string>,
 947 |                       ),
 948 |                   },
 949 |                 },
 950 |               }),
 951 |             ]),
 952 |           }),
 953 |         )
 954 |         .build();
 955 | 
 956 |       const output = formatEventOutput(event);
 957 | 
 958 |       expect(output).toMatchInlineSnapshot(`
 959 |         "### Error
 960 | 
 961 |         \`\`\`
 962 |         ValueError: Data processing error
 963 |         \`\`\`
 964 | 
 965 |         **Most Relevant Frame:**
 966 |         ─────────────────────
 967 |           File "/app/processor.py", line 45, in process_batch
 968 | 
 969 |         Local Variables:
 970 |         ├─ small_array: [1,2,3]
 971 |         ├─ large_array: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26, ...]
 972 |         ├─ small_object: {"name":"test","value":123}
 973 |         └─ large_object: {"data":{"field0":"value0","field1":"value1","field2":"value2", ...}
 974 | 
 975 |         **Full Stacktrace:**
 976 |         ────────────────
 977 |         \`\`\`
 978 |           File "/app/processor.py", line 45, in process_batch
 979 |         \`\`\`
 980 | 
 981 |         "
 982 |       `);
 983 |     });
 984 | 
 985 |     it("should show proper truncation format", () => {
 986 |       const event = new EventBuilder("javascript")
 987 |         .withException(
 988 |           createExceptionValue({
 989 |             type: "Error",
 990 |             value: "Test error",
 991 |             stacktrace: createStackTrace([
 992 |               createFrame({
 993 |                 filename: "/app/test.js",
 994 |                 function: "test",
 995 |                 lineNo: 1,
 996 |                 inApp: true,
 997 |                 vars: {
 998 |                   shortArray: [1, 2, 3],
 999 |                   // This will be over 80 chars when stringified
1000 |                   longArray: [
1001 |                     1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
1002 |                     18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
1003 |                   ],
1004 |                   shortObject: { a: 1, b: 2 },
1005 |                   // This will be over 80 chars when stringified
1006 |                   longObject: {
1007 |                     field1: "value1",
1008 |                     field2: "value2",
1009 |                     field3: "value3",
1010 |                     field4: "value4",
1011 |                     field5: "value5",
1012 |                     field6: "value6",
1013 |                     field7: "value7",
1014 |                     field8: "value8",
1015 |                   },
1016 |                 },
1017 |               }),
1018 |             ]),
1019 |           }),
1020 |         )
1021 |         .build();
1022 | 
1023 |       const output = formatEventOutput(event);
1024 | 
1025 |       expect(output).toMatchInlineSnapshot(`
1026 |         "### Error
1027 | 
1028 |         \`\`\`
1029 |         Error: Test error
1030 |         \`\`\`
1031 | 
1032 |         **Most Relevant Frame:**
1033 |         ─────────────────────
1034 |         /app/test.js:1 (test)
1035 | 
1036 |         Local Variables:
1037 |         ├─ shortArray: [1,2,3]
1038 |         ├─ longArray: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27, ...]
1039 |         ├─ shortObject: {"a":1,"b":2}
1040 |         └─ longObject: {"field1":"value1","field2":"value2","field3":"value3","field4":"value4", ...}
1041 | 
1042 |         **Full Stacktrace:**
1043 |         ────────────────
1044 |         \`\`\`
1045 |         /app/test.js:1 (test)
1046 |         \`\`\`
1047 | 
1048 |         "
1049 |       `);
1050 |     });
1051 | 
1052 |     it("should handle circular references gracefully", () => {
1053 |       const circular: any = { name: "test" };
1054 |       circular.self = circular;
1055 | 
1056 |       const event = new EventBuilder("javascript")
1057 |         .withException(
1058 |           createExceptionValue({
1059 |             type: "TypeError",
1060 |             value: "Circular reference detected",
1061 |             stacktrace: createStackTrace([
1062 |               createFrame({
1063 |                 filename: "/app/utils.js",
1064 |                 function: "serialize",
1065 |                 lineNo: 10,
1066 |                 inApp: true,
1067 |                 vars: {
1068 |                   normal: { a: 1, b: 2 },
1069 |                   circular: circular,
1070 |                 },
1071 |               }),
1072 |             ]),
1073 |           }),
1074 |         )
1075 |         .build();
1076 | 
1077 |       const output = formatEventOutput(event);
1078 | 
1079 |       expect(output).toMatchInlineSnapshot(`
1080 |         "### Error
1081 | 
1082 |         \`\`\`
1083 |         TypeError: Circular reference detected
1084 |         \`\`\`
1085 | 
1086 |         **Most Relevant Frame:**
1087 |         ─────────────────────
1088 |         /app/utils.js:10 (serialize)
1089 | 
1090 |         Local Variables:
1091 |         ├─ normal: {"a":1,"b":2}
1092 |         └─ circular: <object>
1093 | 
1094 |         **Full Stacktrace:**
1095 |         ────────────────
1096 |         \`\`\`
1097 |         /app/utils.js:10 (serialize)
1098 |         \`\`\`
1099 | 
1100 |         "
1101 |       `);
1102 |     });
1103 | 
1104 |     it("should handle empty vars object", () => {
1105 |       const event = createSimpleExceptionEvent(
1106 |         "python",
1107 |         "RuntimeError",
1108 |         "Something went wrong",
1109 |         createFrame({
1110 |           filename: "/app/main.py",
1111 |           function: "main",
1112 |           lineNo: 1,
1113 |           inApp: true,
1114 |           vars: {},
1115 |         }),
1116 |       );
1117 | 
1118 |       const output = formatEventOutput(event);
1119 | 
1120 |       expect(output).toMatchInlineSnapshot(`
1121 |         "### Error
1122 | 
1123 |         \`\`\`
1124 |         RuntimeError: Something went wrong
1125 |         \`\`\`
1126 | 
1127 |         **Most Relevant Frame:**
1128 |         ─────────────────────
1129 |           File "/app/main.py", line 1, in main
1130 | 
1131 |         **Full Stacktrace:**
1132 |         ────────────────
1133 |         \`\`\`
1134 |           File "/app/main.py", line 1, in main
1135 |         \`\`\`
1136 | 
1137 |         "
1138 |       `);
1139 |     });
1140 | 
1141 |     it("should handle large context with proper windowing", () => {
1142 |       const event = createSimpleExceptionEvent(
1143 |         "python",
1144 |         "IndexError",
1145 |         "list index out of range",
1146 |         createFrameWithContext(
1147 |           {
1148 |             filename: "/app/processor.py",
1149 |             function: "process_items",
1150 |             lineNo: 50,
1151 |           },
1152 |           [
1153 |             [45, "    # Setup phase"],
1154 |             [46, "    items = get_items()"],
1155 |             [47, "    results = []"],
1156 |             [48, "    "],
1157 |             [49, "    # This line causes the error"],
1158 |             [50, "    first_item = items[0]"],
1159 |             [51, "    "],
1160 |             [52, "    # Process items"],
1161 |             [53, "    for item in items:"],
1162 |             [54, "        results.append(process(item))"],
1163 |             [55, "    return results"],
1164 |           ],
1165 |           {
1166 |             items: [],
1167 |           },
1168 |         ),
1169 |       );
1170 | 
1171 |       const output = formatEventOutput(event);
1172 | 
1173 |       expect(output).toMatchInlineSnapshot(`
1174 |         "### Error
1175 | 
1176 |         \`\`\`
1177 |         IndexError: list index out of range
1178 |         \`\`\`
1179 | 
1180 |         **Most Relevant Frame:**
1181 |         ─────────────────────
1182 |           File "/app/processor.py", line 50, in process_items
1183 | 
1184 |             47 │     results = []
1185 |             48 │     
1186 |             49 │     # This line causes the error
1187 |           → 50 │     first_item = items[0]
1188 |             51 │     
1189 |             52 │     # Process items
1190 |             53 │     for item in items:
1191 | 
1192 |         Local Variables:
1193 |         └─ items: []
1194 | 
1195 |         **Full Stacktrace:**
1196 |         ────────────────
1197 |         \`\`\`
1198 |           File "/app/processor.py", line 50, in process_items
1199 |             first_item = items[0]
1200 |         \`\`\`
1201 | 
1202 |         "
1203 |       `);
1204 |     });
1205 | 
1206 |     it("should handle context at beginning of file", () => {
1207 |       const event = createSimpleExceptionEvent(
1208 |         "python",
1209 |         "ImportError",
1210 |         "No module named 'missing_module'",
1211 |         createFrameWithContext(
1212 |           {
1213 |             filename: "/app/startup.py",
1214 |             function: "<module>",
1215 |             lineNo: 2,
1216 |           },
1217 |           [
1218 |             [1, "import os"],
1219 |             [2, "import missing_module"],
1220 |             [3, "import json"],
1221 |             [4, ""],
1222 |             [5, "def main():"],
1223 |           ],
1224 |         ),
1225 |       );
1226 | 
1227 |       const output = formatEventOutput(event);
1228 | 
1229 |       expect(output).toMatchInlineSnapshot(`
1230 |         "### Error
1231 | 
1232 |         \`\`\`
1233 |         ImportError: No module named 'missing_module'
1234 |         \`\`\`
1235 | 
1236 |         **Most Relevant Frame:**
1237 |         ─────────────────────
1238 |           File "/app/startup.py", line 2, in <module>
1239 | 
1240 |             1 │ import os
1241 |           → 2 │ import missing_module
1242 |             3 │ import json
1243 |             4 │ 
1244 |             5 │ def main():
1245 | 
1246 |         **Full Stacktrace:**
1247 |         ────────────────
1248 |         \`\`\`
1249 |           File "/app/startup.py", line 2, in <module>
1250 |         import missing_module
1251 |         \`\`\`
1252 | 
1253 |         "
1254 |       `);
1255 |     });
1256 |   });
1257 | 
1258 |   describe("Chained exceptions", () => {
1259 |     it("should render multiple chained exceptions", () => {
1260 |       const event = new EventBuilder("python")
1261 |         .withChainedExceptions([
1262 |           createExceptionValue({
1263 |             type: "KeyError",
1264 |             value: "'user_id'",
1265 |             stacktrace: createStackTrace([
1266 |               createFrame({
1267 |                 filename: "/app/database.py",
1268 |                 function: "get_user",
1269 |                 lineNo: 15,
1270 |                 inApp: true,
1271 |               }),
1272 |             ]),
1273 |           }),
1274 |           createExceptionValue({
1275 |             type: "ValueError",
1276 |             value: "User not found",
1277 |             stacktrace: createStackTrace([
1278 |               createFrame({
1279 |                 filename: "/app/services.py",
1280 |                 function: "process_user",
1281 |                 lineNo: 25,
1282 |                 inApp: true,
1283 |               }),
1284 |             ]),
1285 |           }),
1286 |           createExceptionValue({
1287 |             type: "HTTPError",
1288 |             value: "500 Internal Server Error",
1289 |             stacktrace: createStackTrace([
1290 |               createFrame({
1291 |                 filename: "/app/handlers.py",
1292 |                 function: "handle_request",
1293 |                 lineNo: 42,
1294 |                 inApp: true,
1295 |               }),
1296 |             ]),
1297 |           }),
1298 |         ])
1299 |         .build();
1300 | 
1301 |       const output = formatEventOutput(event);
1302 | 
1303 |       expect(output).toMatchInlineSnapshot(`
1304 |         "### Error
1305 | 
1306 |         \`\`\`
1307 |         HTTPError: 500 Internal Server Error
1308 |         \`\`\`
1309 | 
1310 |         **Stacktrace:**
1311 |         \`\`\`
1312 |           File "/app/handlers.py", line 42, in handle_request
1313 |         \`\`\`
1314 | 
1315 |         **During handling of the above exception, another exception occurred:**
1316 | 
1317 |         ### ValueError: User not found
1318 | 
1319 |         **Stacktrace:**
1320 |         \`\`\`
1321 |           File "/app/services.py", line 25, in process_user
1322 |         \`\`\`
1323 | 
1324 |         **During handling of the above exception, another exception occurred:**
1325 | 
1326 |         ### KeyError: 'user_id'
1327 | 
1328 |         **Stacktrace:**
1329 |         \`\`\`
1330 |           File "/app/database.py", line 15, in get_user
1331 |         \`\`\`
1332 | 
1333 |         "
1334 |       `);
1335 |     });
1336 | 
1337 |     it("should render chained exceptions with enhanced frame on outermost exception", () => {
1338 |       const event = new EventBuilder("python")
1339 |         .withChainedExceptions([
1340 |           createExceptionValue({
1341 |             type: "KeyError",
1342 |             value: "'user_id'",
1343 |             stacktrace: createStackTrace([
1344 |               createFrameWithContext(
1345 |                 {
1346 |                   filename: "/app/database.py",
1347 |                   function: "get_user",
1348 |                   lineNo: 15,
1349 |                   inApp: true,
1350 |                 },
1351 |                 [
1352 |                   [13, "def get_user(data):"],
1353 |                   [14, "    # This will fail if user_id is missing"],
1354 |                   [15, "    user_id = data['user_id']"],
1355 |                   [16, "    return db.find_user(user_id)"],
1356 |                 ],
1357 |                 {
1358 |                   data: {},
1359 |                 },
1360 |               ),
1361 |             ]),
1362 |           }),
1363 |           createExceptionValue({
1364 |             type: "ValueError",
1365 |             value: "User not found",
1366 |             stacktrace: createStackTrace([
1367 |               createFrameWithContext(
1368 |                 {
1369 |                   filename: "/app/services.py",
1370 |                   function: "process_user",
1371 |                   lineNo: 25,
1372 |                   inApp: true,
1373 |                 },
1374 |                 [
1375 |                   [23, "    try:"],
1376 |                   [24, "        user = get_user(request_data)"],
1377 |                   [25, "    except KeyError:"],
1378 |                   [26, "        raise ValueError('User not found')"],
1379 |                 ],
1380 |                 {
1381 |                   request_data: {},
1382 |                 },
1383 |               ),
1384 |             ]),
1385 |           }),
1386 |         ])
1387 |         .build();
1388 | 
1389 |       const output = formatEventOutput(event);
1390 | 
1391 |       expect(output).toMatchInlineSnapshot(`
1392 |         "### Error
1393 | 
1394 |         \`\`\`
1395 |         ValueError: User not found
1396 |         \`\`\`
1397 | 
1398 |         **Most Relevant Frame:**
1399 |         ─────────────────────
1400 |           File "/app/services.py", line 25, in process_user
1401 | 
1402 |             23 │     try:
1403 |             24 │         user = get_user(request_data)
1404 |           → 25 │     except KeyError:
1405 |             26 │         raise ValueError('User not found')
1406 | 
1407 |         Local Variables:
1408 |         └─ request_data: {}
1409 | 
1410 |         **Full Stacktrace:**
1411 |         ────────────────
1412 |         \`\`\`
1413 |           File "/app/services.py", line 25, in process_user
1414 |             except KeyError:
1415 |         \`\`\`
1416 | 
1417 |         **During handling of the above exception, another exception occurred:**
1418 | 
1419 |         ### KeyError: 'user_id'
1420 | 
1421 |         **Stacktrace:**
1422 |         \`\`\`
1423 |           File "/app/database.py", line 15, in get_user
1424 |             user_id = data['user_id']
1425 |         \`\`\`
1426 | 
1427 |         "
1428 |       `);
1429 |     });
1430 | 
1431 |     it("should handle single exception in values array (not chained)", () => {
1432 |       const event = new EventBuilder("python")
1433 |         .withChainedExceptions([
1434 |           createExceptionValue({
1435 |             type: "RuntimeError",
1436 |             value: "Something went wrong",
1437 |             stacktrace: createStackTrace([
1438 |               createFrame({
1439 |                 filename: "/app/main.py",
1440 |                 function: "main",
1441 |                 lineNo: 10,
1442 |                 inApp: true,
1443 |               }),
1444 |             ]),
1445 |           }),
1446 |         ])
1447 |         .build();
1448 | 
1449 |       const output = formatEventOutput(event);
1450 | 
1451 |       expect(output).toMatchInlineSnapshot(`
1452 |         "### Error
1453 | 
1454 |         \`\`\`
1455 |         RuntimeError: Something went wrong
1456 |         \`\`\`
1457 | 
1458 |         **Stacktrace:**
1459 |         \`\`\`
1460 |           File "/app/main.py", line 10, in main
1461 |         \`\`\`
1462 | 
1463 |         "
1464 |       `);
1465 |     });
1466 | 
1467 |     it("should use Java-style 'Caused by' for Java platform", () => {
1468 |       const event = new EventBuilder("java")
1469 |         .withChainedExceptions([
1470 |           createExceptionValue({
1471 |             type: "SQLException",
1472 |             value: "Database connection failed",
1473 |             stacktrace: createStackTrace([
1474 |               frameFactories.java({
1475 |                 module: "com.example.db.DatabaseConnector",
1476 |                 function: "connect",
1477 |                 filename: "DatabaseConnector.java",
1478 |                 lineNo: 45,
1479 |               }),
1480 |             ]),
1481 |           }),
1482 |           createExceptionValue({
1483 |             type: "RuntimeException",
1484 |             value: "Failed to initialize service",
1485 |             stacktrace: createStackTrace([
1486 |               frameFactories.java({
1487 |                 module: "com.example.service.UserService",
1488 |                 function: "initialize",
1489 |                 filename: "UserService.java",
1490 |                 lineNo: 23,
1491 |               }),
1492 |             ]),
1493 |           }),
1494 |         ])
1495 |         .build();
1496 | 
1497 |       const output = formatEventOutput(event);
1498 | 
1499 |       expect(output).toMatchInlineSnapshot(`
1500 |         "### Error
1501 | 
1502 |         \`\`\`
1503 |         RuntimeException: Failed to initialize service
1504 |         \`\`\`
1505 | 
1506 |         **Stacktrace:**
1507 |         \`\`\`
1508 |         at com.example.service.UserService.initialize(UserService.java:23)
1509 |         \`\`\`
1510 | 
1511 |         **Caused by:**
1512 | 
1513 |         ### SQLException: Database connection failed
1514 | 
1515 |         **Stacktrace:**
1516 |         \`\`\`
1517 |         at com.example.db.DatabaseConnector.connect(DatabaseConnector.java:45)
1518 |         \`\`\`
1519 | 
1520 |         "
1521 |       `);
1522 |     });
1523 | 
1524 |     it("should use C#-style arrow notation for dotnet platform", () => {
1525 |       const event = new EventBuilder("csharp")
1526 |         .withChainedExceptions([
1527 |           createExceptionValue({
1528 |             type: "ArgumentNullException",
1529 |             value: "Value cannot be null. (Parameter 'userId')",
1530 |             stacktrace: createStackTrace([
1531 |               createFrame({
1532 |                 filename: "UserRepository.cs",
1533 |                 function: "GetUserById",
1534 |                 lineNo: 15,
1535 |                 inApp: true,
1536 |               }),
1537 |             ]),
1538 |           }),
1539 |           createExceptionValue({
1540 |             type: "ApplicationException",
1541 |             value: "Failed to load user profile",
1542 |             stacktrace: createStackTrace([
1543 |               createFrame({
1544 |                 filename: "UserService.cs",
1545 |                 function: "LoadProfile",
1546 |                 lineNo: 42,
1547 |                 inApp: true,
1548 |               }),
1549 |             ]),
1550 |           }),
1551 |         ])
1552 |         .build();
1553 | 
1554 |       const output = formatEventOutput(event);
1555 | 
1556 |       expect(output).toMatchInlineSnapshot(`
1557 |         "### Error
1558 | 
1559 |         \`\`\`
1560 |         ApplicationException: Failed to load user profile
1561 |         \`\`\`
1562 | 
1563 |         **Stacktrace:**
1564 |         \`\`\`
1565 |             at LoadProfile (UserService.cs:42)
1566 |         \`\`\`
1567 | 
1568 |         **---> Inner Exception:**
1569 | 
1570 |         ### ArgumentNullException: Value cannot be null. (Parameter 'userId')
1571 | 
1572 |         **Stacktrace:**
1573 |         \`\`\`
1574 |             at GetUserById (UserRepository.cs:15)
1575 |         \`\`\`
1576 | 
1577 |         "
1578 |       `);
1579 |     });
1580 | 
1581 |     it("should handle child exception without stacktrace", () => {
1582 |       const event = new EventBuilder("python")
1583 |         .withChainedExceptions([
1584 |           createExceptionValue({
1585 |             type: "KeyError",
1586 |             value: "'missing_key'",
1587 |             // No stacktrace for child exception
1588 |             stacktrace: undefined,
1589 |           }),
1590 |           createExceptionValue({
1591 |             type: "ValueError",
1592 |             value: "Data processing failed",
1593 |             stacktrace: createStackTrace([
1594 |               createFrame({
1595 |                 filename: "/app/processor.py",
1596 |                 function: "process_data",
1597 |                 lineNo: 42,
1598 |                 inApp: true,
1599 |               }),
1600 |             ]),
1601 |           }),
1602 |         ])
1603 |         .build();
1604 | 
1605 |       const output = formatEventOutput(event);
1606 | 
1607 |       expect(output).toMatchInlineSnapshot(`
1608 |         "### Error
1609 | 
1610 |         \`\`\`
1611 |         ValueError: Data processing failed
1612 |         \`\`\`
1613 | 
1614 |         **Stacktrace:**
1615 |         \`\`\`
1616 |           File "/app/processor.py", line 42, in process_data
1617 |         \`\`\`
1618 | 
1619 |         **During handling of the above exception, another exception occurred:**
1620 | 
1621 |         ### KeyError: 'missing_key'
1622 | 
1623 |         **Stacktrace:**
1624 |         \`\`\`
1625 |         No stacktrace available
1626 |         \`\`\`
1627 | 
1628 |         "
1629 |       `);
1630 |     });
1631 |   });
1632 | 
1633 |   describe("Performance issue formatting", () => {
1634 |     it("should format N+1 query issue with evidence data", () => {
1635 |       const event = new EventBuilder("python")
1636 |         .withType("transaction")
1637 |         .withOccurrence({
1638 |           issueTitle: "N+1 Query",
1639 |           culprit: "SELECT * FROM users WHERE id = %s",
1640 |           type: 1006, // Performance issue type code
1641 |           issueType: "performance_n_plus_one_db_queries",
1642 |           evidenceData: {
1643 |             parentSpanIds: ["span_123"],
1644 |             parentSpan: "GET /api/users",
1645 |             repeatingSpansCompact: ["SELECT * FROM users WHERE id = %s"],
1646 |             numberRepeatingSpans: "5",
1647 |             offenderSpanIds: [
1648 |               "span_456",
1649 |               "span_457",
1650 |               "span_458",
1651 |               "span_459",
1652 |               "span_460",
1653 |             ],
1654 |             transactionName: "/api/users",
1655 |             op: "db",
1656 |           },
1657 |           evidenceDisplay: [
1658 |             {
1659 |               name: "Offending Spans",
1660 |               value: "UserService.get_users",
1661 |               important: true,
1662 |             },
1663 |           ],
1664 |         })
1665 |         .build();
1666 | 
1667 |       const output = formatEventOutput(event);
1668 | 
1669 |       expect(output).toMatchInlineSnapshot(`
1670 |         "**Parent Operation:**
1671 |         GET /api/users
1672 | 
1673 |         ### Repeated Database Queries
1674 | 
1675 |         **Query executed 5 times:**
1676 |         \`\`\`sql
1677 |         SELECT * FROM users WHERE id = %s
1678 |         \`\`\`
1679 | 
1680 |         **Transaction:**
1681 |         /api/users
1682 | 
1683 |         **Offending Spans:**
1684 |         UserService.get_users
1685 | 
1686 |         "
1687 |       `);
1688 |     });
1689 | 
1690 |     it("should format N+1 query issue with spans data fallback", () => {
1691 |       const event = new EventBuilder("python")
1692 |         .withType("transaction")
1693 |         .withOccurrence({
1694 |           issueTitle: "N+1 Query detected",
1695 |           culprit: "database query",
1696 |         })
1697 |         .withEntry({
1698 |           type: "spans",
1699 |           data: [
1700 |             {
1701 |               op: "db.query",
1702 |               description: "SELECT * FROM posts WHERE user_id = 1",
1703 |               timestamp: 100.5,
1704 |               start_timestamp: 100.0,
1705 |             },
1706 |             {
1707 |               op: "db.query",
1708 |               description: "SELECT * FROM posts WHERE user_id = 2",
1709 |               timestamp: 101.0,
1710 |               start_timestamp: 100.5,
1711 |             },
1712 |             {
1713 |               op: "db.query",
1714 |               description: "SELECT * FROM posts WHERE user_id = 3",
1715 |               timestamp: 101.5,
1716 |               start_timestamp: 101.0,
1717 |             },
1718 |             {
1719 |               op: "http.client",
1720 |               description: "GET /api/external",
1721 |               timestamp: 102.0,
1722 |               start_timestamp: 101.5,
1723 |             },
1724 |           ],
1725 |         })
1726 |         .build();
1727 | 
1728 |       const output = formatEventOutput(event);
1729 | 
1730 |       expect(output).toMatchInlineSnapshot(`""`);
1731 |     });
1732 | 
1733 |     it("should format transaction event with non-repeated database queries", () => {
1734 |       const event = new EventBuilder("python")
1735 |         .withType("transaction")
1736 |         .withOccurrence({
1737 |           issueTitle: "Slow DB Query",
1738 |           culprit: "database",
1739 |         })
1740 |         .withEntry({
1741 |           type: "spans",
1742 |           data: [
1743 |             {
1744 |               op: "db.query",
1745 |               description: "SELECT COUNT(*) FROM users",
1746 |               timestamp: 100.5,
1747 |               start_timestamp: 100.0,
1748 |             },
1749 |             {
1750 |               op: "db.query",
1751 |               description: "SELECT * FROM settings WHERE key = 'theme'",
1752 |               timestamp: 101.0,
1753 |               start_timestamp: 100.5,
1754 |             },
1755 |           ],
1756 |         })
1757 |         .build();
1758 | 
1759 |       const output = formatEventOutput(event);
1760 | 
1761 |       expect(output).toMatchInlineSnapshot(`""`);
1762 |     });
1763 | 
1764 |     it("should format evidence with operation and offenderSpanIds", () => {
1765 |       const event = new EventBuilder("python")
1766 |         .withType("transaction")
1767 |         .withOccurrence({
1768 |           issueTitle: "N+1 Query",
1769 |           culprit: "database",
1770 |           issueType: "performance_n_plus_one_db_queries",
1771 |           evidenceData: {
1772 |             op: "db",
1773 |             offenderSpanIds: ["span_1", "span_2", "span_3", "span_4", "span_5"],
1774 |             numberRepeatingSpans: "5",
1775 |           },
1776 |         })
1777 |         .build();
1778 | 
1779 |       const output = formatEventOutput(event);
1780 | 
1781 |       expect(output).toMatchInlineSnapshot(`""`);
1782 |     });
1783 | 
1784 |     it("should render span tree for N+1 queries with evidence and spans data", () => {
1785 |       const event = new EventBuilder("python")
1786 |         .withType("transaction")
1787 |         .withOccurrence({
1788 |           issueTitle: "N+1 Query",
1789 |           culprit: "SELECT * FROM users WHERE id = %s",
1790 |           issueType: "performance_n_plus_one_db_queries",
1791 |           evidenceData: {
1792 |             parentSpanIds: ["parent123"],
1793 |             parentSpan: "GET /api/users",
1794 |             offenderSpanIds: ["span1", "span2", "span3"],
1795 |             repeatingSpansCompact: ["SELECT * FROM users WHERE id = %s"],
1796 |             numberRepeatingSpans: "3",
1797 |             op: "db",
1798 |           },
1799 |         })
1800 |         .withEntry({
1801 |           type: "spans",
1802 |           data: [
1803 |             {
1804 |               span_id: "parent123",
1805 |               op: "http.server",
1806 |               description: "GET /users",
1807 |               timestamp: 1722963600.25,
1808 |               start_timestamp: 1722963600.0,
1809 |             },
1810 |             {
1811 |               span_id: "span1",
1812 |               op: "db.query",
1813 |               description: "SELECT * FROM users WHERE id = 1",
1814 |               timestamp: 1722963600.013,
1815 |               start_timestamp: 1722963600.01,
1816 |             },
1817 |             {
1818 |               span_id: "span2",
1819 |               op: "db.query",
1820 |               description: "SELECT * FROM users WHERE id = 2",
1821 |               timestamp: 1722963600.018,
1822 |               start_timestamp: 1722963600.014,
1823 |             },
1824 |             {
1825 |               span_id: "span3",
1826 |               op: "db.query",
1827 |               description: "SELECT * FROM users WHERE id = 3",
1828 |               timestamp: 1722963600.027,
1829 |               start_timestamp: 1722963600.019,
1830 |             },
1831 |           ],
1832 |         })
1833 |         .build();
1834 | 
1835 |       const output = formatEventOutput(event);
1836 | 
1837 |       expect(output).toMatchInlineSnapshot(`
1838 |         "**Parent Operation:**
1839 |         GET /api/users
1840 | 
1841 |         ### Repeated Database Queries
1842 | 
1843 |         **Query executed 3 times:**
1844 |         \`\`\`sql
1845 |         SELECT * FROM users WHERE id = %s
1846 |         \`\`\`
1847 | 
1848 |         ### Span Tree (Limited to 10 spans)
1849 | 
1850 |         \`\`\`
1851 |         GET /users [parent12 · http.server · 250ms]
1852 |            ├─ SELECT * FROM users WHERE id = 1 [span1 · db.query · 3ms] [N+1]
1853 |            ├─ SELECT * FROM users WHERE id = 2 [span2 · db.query · 4ms] [N+1]
1854 |            └─ SELECT * FROM users WHERE id = 3 [span3 · db.query · 8ms] [N+1]
1855 |         \`\`\`
1856 | 
1857 |         "
1858 |       `);
1859 |     });
1860 | 
1861 |     it("should render span tree using duration fields when timestamps are missing", () => {
1862 |       const event = new EventBuilder("python")
1863 |         .withType("transaction")
1864 |         .withOccurrence({
1865 |           issueTitle: "N+1 Query",
1866 |           issueType: "performance_n_plus_one_db_queries",
1867 |           evidenceData: {
1868 |             parentSpanIds: ["parentDur"],
1869 |             parentSpan: "GET /api/durations",
1870 |             offenderSpanIds: ["spanA", "spanB"],
1871 |             repeatingSpansCompact: [
1872 |               "SELECT * FROM durations WHERE bucket = %s",
1873 |             ],
1874 |             numberRepeatingSpans: "2",
1875 |           },
1876 |         })
1877 |         .withEntry({
1878 |           type: "spans",
1879 |           data: [
1880 |             {
1881 |               span_id: "parentDur",
1882 |               op: "http.server",
1883 |               description: "GET /durations",
1884 |               duration: 1250,
1885 |             },
1886 |             {
1887 |               span_id: "spanA",
1888 |               op: "db.query",
1889 |               description: "SELECT * FROM durations WHERE bucket = 'fast'",
1890 |               duration: 0.5,
1891 |             },
1892 |             {
1893 |               span_id: "spanB",
1894 |               op: "db.query",
1895 |               description: "SELECT * FROM durations WHERE bucket = 'slow'",
1896 |               duration: 1500,
1897 |             },
1898 |           ],
1899 |         })
1900 |         .build();
1901 | 
1902 |       const output = formatEventOutput(event);
1903 | 
1904 |       expect(output).toMatchInlineSnapshot(`
1905 |         "**Parent Operation:**
1906 |         GET /api/durations
1907 | 
1908 |         ### Repeated Database Queries
1909 | 
1910 |         **Query executed 2 times:**
1911 |         \`\`\`sql
1912 |         SELECT * FROM durations WHERE bucket = %s
1913 |         \`\`\`
1914 | 
1915 |         ### Span Tree (Limited to 10 spans)
1916 | 
1917 |         \`\`\`
1918 |         GET /durations [parentDu · http.server · 1250ms]
1919 |            ├─ SELECT * FROM durations WHERE bucket = 'fast' [spanA · db.query · 1ms] [N+1]
1920 |            └─ SELECT * FROM durations WHERE bucket = 'slow' [spanB · db.query · 1500ms] [N+1]
1921 |         \`\`\`
1922 | 
1923 |         "
1924 |       `);
1925 |     });
1926 | 
1927 |     it("should handle transaction event without performance data", () => {
1928 |       const event = new EventBuilder("python")
1929 |         .withType("transaction")
1930 |         .withOccurrence({
1931 |           issueTitle: "Generic Performance Issue",
1932 |           culprit: "slow endpoint",
1933 |         })
1934 |         .build();
1935 | 
1936 |       const output = formatEventOutput(event);
1937 | 
1938 |       expect(output).toMatchInlineSnapshot(`""`);
1939 |     });
1940 | 
1941 |     it("should handle evidence data without repeating_spans", () => {
1942 |       const event = new EventBuilder("python")
1943 |         .withType("transaction")
1944 |         .withOccurrence({
1945 |           issueTitle: "Performance Issue",
1946 |           culprit: "database",
1947 |           issueType: "performance_slow_db_query", // A different type that we don't fully handle yet
1948 |           evidenceData: {
1949 |             parentSpan: "GET /api/data",
1950 |             transactionName: "/api/data",
1951 |           },
1952 |           evidenceDisplay: [
1953 |             {
1954 |               name: "Source Location",
1955 |               value: "DataService.fetch",
1956 |               important: true,
1957 |             },
1958 |           ],
1959 |         })
1960 |         .build();
1961 | 
1962 |       const output = formatEventOutput(event);
1963 | 
1964 |       expect(output).toMatchInlineSnapshot(`
1965 |         "**Parent Operation:**
1966 |         GET /api/data
1967 | 
1968 |         **Transaction:**
1969 |         /api/data
1970 | 
1971 |         **Source Location:**
1972 |         DataService.fetch
1973 | 
1974 |         "
1975 |       `);
1976 |     });
1977 |   });
1978 | });
1979 | 
```

--------------------------------------------------------------------------------
/packages/mcp-core/src/internal/formatting.ts:
--------------------------------------------------------------------------------

```typescript
   1 | /**
   2 |  * LLM response formatting utilities for Sentry data.
   3 |  *
   4 |  * Converts Sentry API responses into structured markdown format optimized
   5 |  * for LLM consumption. Handles stacktraces, event details, issue summaries,
   6 |  * and contextual information with consistent formatting patterns.
   7 |  */
   8 | import type { z } from "zod";
   9 | import type {
  10 |   Event,
  11 |   Issue,
  12 |   AutofixRunState,
  13 |   Trace,
  14 |   TraceSpan,
  15 |   GenericEvent,
  16 | } from "../api-client/types";
  17 | import type {
  18 |   ErrorEntrySchema,
  19 |   ErrorEventSchema,
  20 |   DefaultEventSchema,
  21 |   GenericEventSchema,
  22 |   EventSchema,
  23 |   FrameInterface,
  24 |   RequestEntrySchema,
  25 |   MessageEntrySchema,
  26 |   ThreadsEntrySchema,
  27 |   SentryApiService,
  28 |   AutofixRunStepRootCauseAnalysisSchema,
  29 | } from "../api-client";
  30 | import {
  31 |   getOutputForAutofixStep,
  32 |   isTerminalStatus,
  33 |   getStatusDisplayName,
  34 | } from "./tool-helpers/seer";
  35 | import { logIssue } from "../telem/logging";
  36 | 
  37 | // Language detection mappings
  38 | const LANGUAGE_EXTENSIONS: Record<string, string> = {
  39 |   ".java": "java",
  40 |   ".py": "python",
  41 |   ".js": "javascript",
  42 |   ".jsx": "javascript",
  43 |   ".ts": "javascript",
  44 |   ".tsx": "javascript",
  45 |   ".rb": "ruby",
  46 |   ".php": "php",
  47 | };
  48 | 
  49 | const LANGUAGE_MODULE_PATTERNS: Array<[RegExp, string]> = [
  50 |   [/^(java\.|com\.|org\.)/, "java"],
  51 | ];
  52 | 
  53 | /**
  54 |  * Detects the programming language of a stack frame based on the file extension.
  55 |  * Falls back to the platform parameter if no filename is available or extension is unrecognized.
  56 |  *
  57 |  * @param frame - The stack frame containing file and location information
  58 |  * @param platform - Optional platform hint to use as fallback
  59 |  * @returns The detected language or platform fallback or "unknown"
  60 |  */
  61 | function detectLanguage(
  62 |   frame: z.infer<typeof FrameInterface>,
  63 |   platform?: string | null,
  64 | ): string {
  65 |   // Check filename extensions
  66 |   if (frame.filename) {
  67 |     const ext = frame.filename.toLowerCase().match(/\.[^.]+$/)?.[0];
  68 |     if (ext && LANGUAGE_EXTENSIONS[ext]) {
  69 |       return LANGUAGE_EXTENSIONS[ext];
  70 |     }
  71 |   }
  72 | 
  73 |   // Check module patterns
  74 |   if (frame.module) {
  75 |     for (const [pattern, language] of LANGUAGE_MODULE_PATTERNS) {
  76 |       if (pattern.test(frame.module)) {
  77 |         return language;
  78 |       }
  79 |     }
  80 |   }
  81 | 
  82 |   // Fallback to platform or unknown
  83 |   return platform || "unknown";
  84 | }
  85 | 
  86 | /**
  87 |  * Formats a stack frame into a language-specific string representation.
  88 |  * Different languages have different conventions for displaying stack traces.
  89 |  *
  90 |  * @param frame - The stack frame to format
  91 |  * @param frameIndex - Optional frame index for languages that display frame numbers
  92 |  * @param platform - Optional platform hint for language detection fallback
  93 |  * @returns Formatted stack frame string
  94 |  */
  95 | export function formatFrameHeader(
  96 |   frame: z.infer<typeof FrameInterface>,
  97 |   frameIndex?: number,
  98 |   platform?: string | null,
  99 | ) {
 100 |   const language = detectLanguage(frame, platform);
 101 | 
 102 |   switch (language) {
 103 |     case "java": {
 104 |       // at com.example.ClassName.methodName(FileName.java:123)
 105 |       const className = frame.module || "UnknownClass";
 106 |       const method = frame.function || "<unknown>";
 107 |       const source = frame.filename || "Unknown Source";
 108 |       const location = frame.lineNo ? `:${frame.lineNo}` : "";
 109 |       return `at ${className}.${method}(${source}${location})`;
 110 |     }
 111 | 
 112 |     case "python": {
 113 |       // File "/path/to/file.py", line 42, in function_name
 114 |       const file =
 115 |         frame.filename || frame.absPath || frame.module || "<unknown>";
 116 |       const func = frame.function || "<module>";
 117 |       const line = frame.lineNo ? `, line ${frame.lineNo}` : "";
 118 |       return `  File "${file}"${line}, in ${func}`;
 119 |     }
 120 | 
 121 |     case "javascript": {
 122 |       // Original compact format: filename:line:col (function)
 123 |       // This preserves backward compatibility
 124 |       return `${[frame.filename, frame.lineNo, frame.colNo]
 125 |         .filter((i) => !!i)
 126 |         .join(":")}${frame.function ? ` (${frame.function})` : ""}`;
 127 |     }
 128 | 
 129 |     case "ruby": {
 130 |       // from /path/to/file.rb:42:in `method_name'
 131 |       const file = frame.filename || frame.module || "<unknown>";
 132 |       const func = frame.function ? ` \`${frame.function}\`` : "";
 133 |       const line = frame.lineNo ? `:${frame.lineNo}:in` : "";
 134 |       return `    from ${file}${line}${func}`;
 135 |     }
 136 | 
 137 |     case "php": {
 138 |       // #0 /path/to/file.php(42): functionName()
 139 |       const file = frame.filename || "<unknown>";
 140 |       const line = frame.lineNo ? `(${frame.lineNo})` : "";
 141 |       const func = frame.function || "<unknown>";
 142 |       const prefix = frameIndex !== undefined ? `#${frameIndex} ` : "";
 143 |       return `${prefix}${file}${line}: ${func}()`;
 144 |     }
 145 | 
 146 |     default: {
 147 |       // Generic format for unknown languages
 148 |       const func = frame.function || "<unknown>";
 149 |       const location = frame.filename || frame.module || "<unknown>";
 150 |       const line = frame.lineNo ? `:${frame.lineNo}` : "";
 151 |       const col = frame.colNo != null ? `:${frame.colNo}` : "";
 152 |       return `    at ${func} (${location}${line}${col})`;
 153 |     }
 154 |   }
 155 | }
 156 | 
 157 | /**
 158 |  * Formats a Sentry event into a structured markdown output.
 159 |  * Includes error messages, stack traces, request info, and contextual data.
 160 |  *
 161 |  * @param event - The Sentry event to format
 162 |  * @param options - Additional formatting context
 163 |  * @returns Formatted markdown string
 164 |  */
 165 | export function formatEventOutput(
 166 |   event: Event,
 167 |   options?: {
 168 |     performanceTrace?: Trace;
 169 |   },
 170 | ) {
 171 |   let output = "";
 172 | 
 173 |   // Check if entries exist (may be undefined for unsupported event types)
 174 |   if (!event.entries || !Array.isArray(event.entries)) {
 175 |     // For unsupported event types, just show tags and contexts
 176 |     output += formatTags(event.tags);
 177 |     output += formatContext(event.context);
 178 |     output += formatContexts(event.contexts);
 179 |     return output;
 180 |   }
 181 | 
 182 |   // Look for the primary error information
 183 |   const messageEntry = event.entries.find((e) => e.type === "message");
 184 |   const exceptionEntry = event.entries.find((e) => e.type === "exception");
 185 |   const threadsEntry = event.entries.find((e) => e.type === "threads");
 186 |   const requestEntry = event.entries.find((e) => e.type === "request");
 187 |   const spansEntry = event.entries.find((e) => e.type === "spans");
 188 |   const cspEntry = event.entries.find((e) => e.type === "csp");
 189 | 
 190 |   // Error message (if present)
 191 |   if (messageEntry) {
 192 |     output += formatMessageInterfaceOutput(
 193 |       event,
 194 |       messageEntry.data as z.infer<typeof MessageEntrySchema>,
 195 |     );
 196 |   }
 197 | 
 198 |   // Stack trace (from exception or threads)
 199 |   if (exceptionEntry) {
 200 |     output += formatExceptionInterfaceOutput(
 201 |       event,
 202 |       exceptionEntry.data as z.infer<typeof ErrorEntrySchema>,
 203 |     );
 204 |   } else if (threadsEntry) {
 205 |     output += formatThreadsInterfaceOutput(
 206 |       event,
 207 |       threadsEntry.data as z.infer<typeof ThreadsEntrySchema>,
 208 |     );
 209 |   }
 210 | 
 211 |   // Request info (if HTTP error)
 212 |   if (requestEntry) {
 213 |     output += formatRequestInterfaceOutput(
 214 |       event,
 215 |       requestEntry.data as z.infer<typeof RequestEntrySchema>,
 216 |     );
 217 |   }
 218 | 
 219 |   // CSP violation details
 220 |   if (cspEntry) {
 221 |     output += formatCspInterfaceOutput(event, cspEntry.data);
 222 |   }
 223 | 
 224 |   // Performance issue details (N+1 queries, etc.)
 225 |   // Pass spans data for additional context even if we have evidence
 226 |   if (event.type === "transaction") {
 227 |     output += formatPerformanceIssueOutput(event, spansEntry?.data, options);
 228 |   }
 229 | 
 230 |   // Generic events (performance regressions, metric-based issues)
 231 |   // These have occurrence data with evidenceDisplay that needs formatting
 232 |   if (event.type === "generic") {
 233 |     output += formatGenericEventOutput(event);
 234 |   }
 235 | 
 236 |   output += formatTags(event.tags);
 237 |   output += formatContext(event.context);
 238 |   output += formatContexts(event.contexts);
 239 |   return output;
 240 | }
 241 | 
 242 | /**
 243 |  * Extracts the context line matching the frame's line number for inline display.
 244 |  * This is used in the full stacktrace view to show the actual line of code
 245 |  * that caused the error inline with the stack frame.
 246 |  *
 247 |  * @param frame - The stack frame containing context lines
 248 |  * @returns The line of code at the frame's line number, or empty string if not available
 249 |  */
 250 | function renderInlineContext(frame: z.infer<typeof FrameInterface>): string {
 251 |   if (!frame.context?.length || !frame.lineNo) {
 252 |     return "";
 253 |   }
 254 | 
 255 |   const contextLine = frame.context.find(([lineNo]) => lineNo === frame.lineNo);
 256 |   return contextLine ? `\n${contextLine[1]}` : "";
 257 | }
 258 | 
 259 | /**
 260 |  * Renders an enhanced view of a stack frame with context lines and variables.
 261 |  * Used for the "Most Relevant Frame" section to provide detailed information
 262 |  * about the most relevant application frame where the error occurred.
 263 |  *
 264 |  * @param frame - The stack frame to render with enhanced information
 265 |  * @param event - The Sentry event containing platform information for language detection
 266 |  * @returns Formatted string with frame header, context lines, and variables table
 267 |  */
 268 | function renderEnhancedFrame(
 269 |   frame: z.infer<typeof FrameInterface>,
 270 |   event: Event,
 271 | ): string {
 272 |   const parts: string[] = [];
 273 | 
 274 |   parts.push("**Most Relevant Frame:**");
 275 |   parts.push("─────────────────────");
 276 |   parts.push(formatFrameHeader(frame, undefined, event.platform));
 277 | 
 278 |   // Add context lines if available
 279 |   if (frame.context?.length) {
 280 |     const contextLines = renderContextLines(frame);
 281 |     if (contextLines) {
 282 |       parts.push("");
 283 |       parts.push(contextLines);
 284 |     }
 285 |   }
 286 | 
 287 |   // Add variables table if available
 288 |   if (frame.vars && Object.keys(frame.vars).length > 0) {
 289 |     parts.push("");
 290 |     parts.push(renderVariablesTable(frame.vars));
 291 |   }
 292 | 
 293 |   return parts.join("\n");
 294 | }
 295 | 
 296 | function formatExceptionInterfaceOutput(
 297 |   event: Event,
 298 |   data: z.infer<typeof ErrorEntrySchema>,
 299 | ) {
 300 |   const parts: string[] = [];
 301 | 
 302 |   // Handle both single exception (value) and chained exceptions (values)
 303 |   const exceptions = data.values || (data.value ? [data.value] : []);
 304 | 
 305 |   if (exceptions.length === 0) {
 306 |     return "";
 307 |   }
 308 | 
 309 |   // For chained exceptions, they are typically ordered from innermost to outermost
 310 |   // We'll render them in reverse order (outermost first) to match how they occurred
 311 |   const isChained = exceptions.length > 1;
 312 | 
 313 |   // Create a copy before reversing to avoid mutating the original array
 314 |   [...exceptions].reverse().forEach((exception, index) => {
 315 |     if (!exception) return;
 316 | 
 317 |     // Add language-specific chain indicator for multiple exceptions
 318 |     if (isChained && index > 0) {
 319 |       parts.push("");
 320 |       parts.push(
 321 |         getExceptionChainMessage(
 322 |           event.platform || null,
 323 |           index,
 324 |           exceptions.length,
 325 |         ),
 326 |       );
 327 |       parts.push("");
 328 |     }
 329 | 
 330 |     // Use the actual exception type and value as the heading
 331 |     const exceptionTitle = `${exception.type}${exception.value ? `: ${exception.value}` : ""}`;
 332 | 
 333 |     parts.push(index === 0 ? "### Error" : `### ${exceptionTitle}`);
 334 |     parts.push("");
 335 | 
 336 |     // Add the error details in a code block for the first exception
 337 |     // to maintain backward compatibility
 338 |     if (index === 0) {
 339 |       parts.push("```");
 340 |       parts.push(exceptionTitle);
 341 |       parts.push("```");
 342 |       parts.push("");
 343 |     }
 344 | 
 345 |     if (!exception.stacktrace || !exception.stacktrace.frames) {
 346 |       parts.push("**Stacktrace:**");
 347 |       parts.push("```");
 348 |       parts.push("No stacktrace available");
 349 |       parts.push("```");
 350 |       return;
 351 |     }
 352 | 
 353 |     const frames = exception.stacktrace.frames;
 354 | 
 355 |     // Only show enhanced frame for the first (outermost) exception to avoid overwhelming output
 356 |     if (index === 0) {
 357 |       const firstInAppFrame = findFirstInAppFrame(frames);
 358 |       if (
 359 |         firstInAppFrame &&
 360 |         (firstInAppFrame.context?.length || firstInAppFrame.vars)
 361 |       ) {
 362 |         parts.push(renderEnhancedFrame(firstInAppFrame, event));
 363 |         parts.push("");
 364 |         parts.push("**Full Stacktrace:**");
 365 |         parts.push("────────────────");
 366 |       } else {
 367 |         parts.push("**Stacktrace:**");
 368 |       }
 369 |     } else {
 370 |       parts.push("**Stacktrace:**");
 371 |     }
 372 | 
 373 |     parts.push("```");
 374 |     parts.push(
 375 |       frames
 376 |         .map((frame) => {
 377 |           const header = formatFrameHeader(frame, undefined, event.platform);
 378 |           const context = renderInlineContext(frame);
 379 |           return `${header}${context}`;
 380 |         })
 381 |         .join("\n"),
 382 |     );
 383 |     parts.push("```");
 384 |   });
 385 | 
 386 |   parts.push("");
 387 |   parts.push("");
 388 | 
 389 |   return parts.join("\n");
 390 | }
 391 | 
 392 | /**
 393 |  * Get the appropriate exception chain message based on the platform
 394 |  */
 395 | function getExceptionChainMessage(
 396 |   platform: string | null,
 397 |   index: number,
 398 |   totalExceptions: number,
 399 | ): string {
 400 |   // Default message for unknown platforms
 401 |   const defaultMessage =
 402 |     "**During handling of the above exception, another exception occurred:**";
 403 | 
 404 |   if (!platform) {
 405 |     return defaultMessage;
 406 |   }
 407 | 
 408 |   switch (platform.toLowerCase()) {
 409 |     case "python":
 410 |       // Python has two distinct messages, but without additional metadata
 411 |       // we default to the implicit chaining message
 412 |       return "**During handling of the above exception, another exception occurred:**";
 413 | 
 414 |     case "java":
 415 |       return "**Caused by:**";
 416 | 
 417 |     case "csharp":
 418 |     case "dotnet":
 419 |       return "**---> Inner Exception:**";
 420 | 
 421 |     case "ruby":
 422 |       return "**Caused by:**";
 423 | 
 424 |     case "go":
 425 |       return "**Wrapped error:**";
 426 | 
 427 |     case "rust":
 428 |       return `**Caused by (${index}):**`;
 429 | 
 430 |     default:
 431 |       return defaultMessage;
 432 |   }
 433 | }
 434 | 
 435 | function formatCspInterfaceOutput(event: Event, data: any) {
 436 |   if (!data) {
 437 |     return "";
 438 |   }
 439 | 
 440 |   const parts: string[] = [];
 441 |   parts.push("### CSP Violation");
 442 |   parts.push("");
 443 | 
 444 |   if (data.blocked_uri) {
 445 |     parts.push(`**Blocked URI**: ${data.blocked_uri}`);
 446 |   }
 447 | 
 448 |   if (data.violated_directive) {
 449 |     parts.push(`**Violated Directive**: ${data.violated_directive}`);
 450 |   }
 451 | 
 452 |   if (data.effective_directive) {
 453 |     parts.push(`**Effective Directive**: ${data.effective_directive}`);
 454 |   }
 455 | 
 456 |   if (data.document_uri) {
 457 |     parts.push(`**Document URI**: ${data.document_uri}`);
 458 |   }
 459 | 
 460 |   if (data.source_file) {
 461 |     parts.push(`**Source File**: ${data.source_file}`);
 462 |     if (data.line_number) {
 463 |       parts.push(`**Line Number**: ${data.line_number}`);
 464 |     }
 465 |   }
 466 | 
 467 |   if (data.disposition) {
 468 |     parts.push(`**Disposition**: ${data.disposition}`);
 469 |   }
 470 | 
 471 |   if (data.original_policy) {
 472 |     parts.push("");
 473 |     parts.push("**Original Policy:**");
 474 |     parts.push("```");
 475 |     parts.push(data.original_policy);
 476 |     parts.push("```");
 477 |   }
 478 | 
 479 |   parts.push("");
 480 |   parts.push("");
 481 | 
 482 |   return parts.join("\n");
 483 | }
 484 | 
 485 | function formatRequestInterfaceOutput(
 486 |   event: Event,
 487 |   data: z.infer<typeof RequestEntrySchema>,
 488 | ) {
 489 |   if (!data.method || !data.url) {
 490 |     return "";
 491 |   }
 492 |   return `### HTTP Request\n\n**Method:** ${data.method}\n**URL:** ${data.url}\n\n`;
 493 | }
 494 | 
 495 | function formatMessageInterfaceOutput(
 496 |   event: Event,
 497 |   data: z.infer<typeof MessageEntrySchema>,
 498 | ) {
 499 |   if (!data.formatted && !data.message) {
 500 |     return "";
 501 |   }
 502 |   const message = data.formatted || data.message || "";
 503 |   return `### Error\n\n${"```"}\n${message}\n${"```"}\n\n`;
 504 | }
 505 | 
 506 | function formatThreadsInterfaceOutput(
 507 |   event: Event,
 508 |   data: z.infer<typeof ThreadsEntrySchema>,
 509 | ) {
 510 |   if (!data.values || data.values.length === 0) {
 511 |     return "";
 512 |   }
 513 | 
 514 |   // Find the crashed thread only
 515 |   const crashedThread = data.values.find((t) => t.crashed);
 516 | 
 517 |   if (!crashedThread?.stacktrace?.frames) {
 518 |     return "";
 519 |   }
 520 | 
 521 |   const parts: string[] = [];
 522 | 
 523 |   // Include thread name if available
 524 |   if (crashedThread.name) {
 525 |     parts.push(`**Thread** (${crashedThread.name})`);
 526 |     parts.push("");
 527 |   }
 528 | 
 529 |   const frames = crashedThread.stacktrace.frames;
 530 | 
 531 |   // Find and format the first in-app frame with enhanced view
 532 |   const firstInAppFrame = findFirstInAppFrame(frames);
 533 |   if (
 534 |     firstInAppFrame &&
 535 |     (firstInAppFrame.context?.length || firstInAppFrame.vars)
 536 |   ) {
 537 |     parts.push(renderEnhancedFrame(firstInAppFrame, event));
 538 |     parts.push("");
 539 |     parts.push("**Full Stacktrace:**");
 540 |     parts.push("────────────────");
 541 |   } else {
 542 |     parts.push("**Stacktrace:**");
 543 |   }
 544 | 
 545 |   parts.push("```");
 546 |   parts.push(
 547 |     frames
 548 |       .map((frame) => {
 549 |         const header = formatFrameHeader(frame, undefined, event.platform);
 550 |         const context = renderInlineContext(frame);
 551 |         return `${header}${context}`;
 552 |       })
 553 |       .join("\n"),
 554 |   );
 555 |   parts.push("```");
 556 |   parts.push("");
 557 | 
 558 |   return parts.join("\n");
 559 | }
 560 | 
 561 | /**
 562 |  * Renders surrounding source code context for a stack frame.
 563 |  * Shows a window of code lines around the error line with visual indicators.
 564 |  *
 565 |  * @param frame - The stack frame containing context lines
 566 |  * @param contextSize - Number of lines to show before and after the error line (default: 3)
 567 |  * @returns Formatted context lines with line numbers and arrow indicator for the error line
 568 |  */
 569 | function renderContextLines(
 570 |   frame: z.infer<typeof FrameInterface>,
 571 |   contextSize = 3,
 572 | ): string {
 573 |   if (!frame.context || frame.context.length === 0 || !frame.lineNo) {
 574 |     return "";
 575 |   }
 576 | 
 577 |   const lines: string[] = [];
 578 |   const errorLine = frame.lineNo;
 579 |   const maxLineNoWidth = Math.max(
 580 |     ...frame.context.map(([lineNo]) => lineNo.toString().length),
 581 |   );
 582 | 
 583 |   for (const [lineNo, code] of frame.context) {
 584 |     const isErrorLine = lineNo === errorLine;
 585 |     const lineNoStr = lineNo.toString().padStart(maxLineNoWidth, " ");
 586 | 
 587 |     if (Math.abs(lineNo - errorLine) <= contextSize) {
 588 |       if (isErrorLine) {
 589 |         lines.push(`  → ${lineNoStr} │ ${code}`);
 590 |       } else {
 591 |         lines.push(`    ${lineNoStr} │ ${code}`);
 592 |       }
 593 |     }
 594 |   }
 595 | 
 596 |   return lines.join("\n");
 597 | }
 598 | 
 599 | /**
 600 |  * Formats a variable value for display in the variables table.
 601 |  * Handles different types appropriately and safely, converting complex objects
 602 |  * to readable representations and handling edge cases like circular references.
 603 |  *
 604 |  * @param value - The variable value to format (can be any type)
 605 |  * @param maxLength - Maximum length for stringified objects/arrays (default: 80)
 606 |  * @returns Human-readable string representation of the value
 607 |  */
 608 | function formatVariableValue(value: unknown, maxLength = 80): string {
 609 |   try {
 610 |     if (typeof value === "string") {
 611 |       return `"${value}"`;
 612 |     }
 613 |     if (value === null) {
 614 |       return "null";
 615 |     }
 616 |     if (value === undefined) {
 617 |       return "undefined";
 618 |     }
 619 |     if (typeof value === "object") {
 620 |       const stringified = JSON.stringify(value);
 621 |       if (stringified.length > maxLength) {
 622 |         // Leave room for ", ...]" or ", ...}"
 623 |         const truncateAt = maxLength - 6;
 624 |         let truncated = stringified.substring(0, truncateAt);
 625 | 
 626 |         // Find the last complete element by looking for the last comma
 627 |         const lastComma = truncated.lastIndexOf(",");
 628 |         if (lastComma > 0) {
 629 |           truncated = truncated.substring(0, lastComma);
 630 |         }
 631 | 
 632 |         // Add the appropriate ending
 633 |         if (Array.isArray(value)) {
 634 |           return `${truncated}, ...]`;
 635 |         }
 636 |         return `${truncated}, ...}`;
 637 |       }
 638 |       return stringified;
 639 |     }
 640 |     return String(value);
 641 |   } catch {
 642 |     // Handle circular references or other stringify errors
 643 |     return `<${typeof value}>`;
 644 |   }
 645 | }
 646 | 
 647 | /**
 648 |  * Renders a table of local variables in a tree-like format.
 649 |  * Uses box-drawing characters to create a visual hierarchy of variables
 650 |  * and their values at the point where the error occurred.
 651 |  *
 652 |  * @param vars - Object containing variable names as keys and their values
 653 |  * @returns Formatted variables table with tree-style prefix characters
 654 |  */
 655 | function renderVariablesTable(vars: Record<string, unknown>): string {
 656 |   const entries = Object.entries(vars);
 657 |   if (entries.length === 0) {
 658 |     return "";
 659 |   }
 660 | 
 661 |   const lines: string[] = ["Local Variables:"];
 662 |   const lastIndex = entries.length - 1;
 663 | 
 664 |   entries.forEach(([key, value], index) => {
 665 |     const prefix = index === lastIndex ? "└─" : "├─";
 666 |     const valueStr = formatVariableValue(value);
 667 |     lines.push(`${prefix} ${key}: ${valueStr}`);
 668 |   });
 669 | 
 670 |   return lines.join("\n");
 671 | }
 672 | 
 673 | /**
 674 |  * Finds the first application frame (in_app) in a stack trace.
 675 |  * Searches from the bottom of the stack (oldest frame) to find the first
 676 |  * frame that belongs to the user's application code rather than libraries.
 677 |  *
 678 |  * @param frames - Array of stack frames, typically in reverse chronological order
 679 |  * @returns The first in-app frame found, or undefined if none exist
 680 |  */
 681 | function findFirstInAppFrame(
 682 |   frames: z.infer<typeof FrameInterface>[],
 683 | ): z.infer<typeof FrameInterface> | undefined {
 684 |   // Frames are usually in reverse order (most recent first)
 685 |   // We want the first in-app frame from the bottom
 686 |   for (let i = frames.length - 1; i >= 0; i--) {
 687 |     if (frames[i].inApp === true) {
 688 |       return frames[i];
 689 |     }
 690 |   }
 691 |   return undefined;
 692 | }
 693 | 
 694 | /**
 695 |  * Constants for performance issue formatting
 696 |  */
 697 | const MAX_SPANS_IN_TREE = 10;
 698 | 
 699 | /**
 700 |  * Safely parse a number from a string, returning a default if invalid
 701 |  */
 702 | function safeParseInt(value: unknown, defaultValue: number): number {
 703 |   if (typeof value === "number") return value;
 704 |   if (typeof value === "string") {
 705 |     const parsed = Number.parseInt(value, 10);
 706 |     return Number.isNaN(parsed) ? defaultValue : parsed;
 707 |   }
 708 |   return defaultValue;
 709 | }
 710 | 
 711 | /**
 712 |  * Simplified span structure for rendering span trees in performance issues.
 713 |  * This is a subset of the full span data focused on visualization needs.
 714 |  */
 715 | interface PerformanceSpan {
 716 |   span_id: string;
 717 |   op: string; // Operation type (e.g., "db.query", "http.client")
 718 |   description: string; // Human-readable description of what the span did
 719 |   duration: number; // Duration in milliseconds
 720 |   is_n1_query: boolean; // Whether this span is part of the N+1 pattern
 721 |   children: PerformanceSpan[];
 722 |   level: number; // Nesting level for tree rendering
 723 | }
 724 | 
 725 | interface RawSpan {
 726 |   span_id?: string;
 727 |   id?: string;
 728 |   op?: string;
 729 |   description?: string;
 730 |   timestamp?: number;
 731 |   start_timestamp?: number;
 732 |   duration?: number;
 733 | }
 734 | 
 735 | interface N1EvidenceData {
 736 |   parentSpan?: string;
 737 |   parentSpanIds?: string[];
 738 |   repeatingSpansCompact?: string[];
 739 |   repeatingSpans?: string[];
 740 |   numberRepeatingSpans?: string; // API returns string even though it's a number
 741 |   numPatternRepetitions?: number;
 742 |   offenderSpanIds?: string[];
 743 |   transactionName?: string;
 744 |   [key: string]: unknown;
 745 | }
 746 | 
 747 | interface SlowDbEvidenceData {
 748 |   parentSpan?: string;
 749 |   [key: string]: unknown;
 750 | }
 751 | 
 752 | function normalizeSpanId(value: unknown): string | undefined {
 753 |   if (typeof value === "string" && value) {
 754 |     return value;
 755 |   }
 756 |   return undefined;
 757 | }
 758 | 
 759 | function getSpanIdentifier(span: RawSpan): string | undefined {
 760 |   if (span.span_id !== undefined) {
 761 |     return normalizeSpanId(span.span_id);
 762 |   }
 763 |   if (span.id !== undefined) {
 764 |     return normalizeSpanId(span.id);
 765 |   }
 766 |   return undefined;
 767 | }
 768 | 
 769 | function getSpanDurationMs(span: RawSpan): number {
 770 |   if (
 771 |     typeof span.timestamp === "number" &&
 772 |     typeof span.start_timestamp === "number"
 773 |   ) {
 774 |     const deltaSeconds = span.timestamp - span.start_timestamp;
 775 |     if (Number.isFinite(deltaSeconds) && deltaSeconds >= 0) {
 776 |       return deltaSeconds * 1000;
 777 |     }
 778 |   }
 779 | 
 780 |   // Trace APIs expose `duration` in milliseconds. Preserve fractional values.
 781 |   if (typeof span.duration === "number" && Number.isFinite(span.duration)) {
 782 |     return span.duration >= 0 ? span.duration : 0;
 783 |   }
 784 | 
 785 |   return 0;
 786 | }
 787 | 
 788 | function normalizeIdArray(values: unknown): string[] {
 789 |   if (!Array.isArray(values)) {
 790 |     return [];
 791 |   }
 792 | 
 793 |   return values
 794 |     .map((value) => normalizeSpanId(value))
 795 |     .filter((value): value is string => value !== undefined);
 796 | }
 797 | 
 798 | function isValidSpanArray(value: unknown): value is RawSpan[] {
 799 |   return Array.isArray(value);
 800 | }
 801 | 
 802 | /**
 803 |  * Get the repeating span descriptions from evidence data.
 804 |  * Prefers repeatingSpansCompact (more concise) over repeatingSpans (verbose).
 805 |  */
 806 | function getRepeatingSpanLines(evidenceData: N1EvidenceData): string[] {
 807 |   // Try compact version first (preferred for display)
 808 |   if (
 809 |     Array.isArray(evidenceData.repeatingSpansCompact) &&
 810 |     evidenceData.repeatingSpansCompact.length > 0
 811 |   ) {
 812 |     return evidenceData.repeatingSpansCompact
 813 |       .map((s) => (typeof s === "string" ? s.trim() : ""))
 814 |       .filter((s): s is string => s.length > 0);
 815 |   }
 816 | 
 817 |   // Fall back to full version
 818 |   if (
 819 |     Array.isArray(evidenceData.repeatingSpans) &&
 820 |     evidenceData.repeatingSpans.length > 0
 821 |   ) {
 822 |     return evidenceData.repeatingSpans
 823 |       .map((s) => (typeof s === "string" ? s.trim() : ""))
 824 |       .filter((s): s is string => s.length > 0);
 825 |   }
 826 | 
 827 |   return [];
 828 | }
 829 | 
 830 | function isTraceSpan(node: unknown): node is TraceSpan {
 831 |   if (node === null || typeof node !== "object") {
 832 |     return false;
 833 |   }
 834 |   const candidate = node as { event_type?: unknown; event_id?: unknown };
 835 |   // Trace API returns spans with event_type: "span"
 836 |   return (
 837 |     candidate.event_type === "span" && typeof candidate.event_id === "string"
 838 |   );
 839 | }
 840 | 
 841 | function buildTraceSpanTree(
 842 |   trace: Trace,
 843 |   parentSpanIds: string[],
 844 |   offenderSpanIds: string[],
 845 |   maxSpans: number,
 846 | ): string[] {
 847 |   const offenderSet = new Set(offenderSpanIds);
 848 |   const spanMap = new Map<string, TraceSpan>();
 849 | 
 850 |   function indexSpan(span: TraceSpan): void {
 851 |     // Try to get span_id from additional_attributes, fall back to event_id
 852 |     const spanId =
 853 |       normalizeSpanId(span.additional_attributes?.span_id) || span.event_id;
 854 |     if (spanId && spanId.length > 0) {
 855 |       spanMap.set(spanId, span);
 856 |     }
 857 |     for (const child of span.children ?? []) {
 858 |       if (isTraceSpan(child)) {
 859 |         indexSpan(child);
 860 |       }
 861 |     }
 862 |   }
 863 | 
 864 |   for (const node of trace) {
 865 |     if (isTraceSpan(node)) {
 866 |       indexSpan(node);
 867 |     }
 868 |   }
 869 | 
 870 |   const roots: PerformanceSpan[] = [];
 871 |   const budget = { count: 0, limit: maxSpans };
 872 | 
 873 |   // First, try to find parent spans
 874 |   for (const parentId of parentSpanIds) {
 875 |     const span = spanMap.get(parentId);
 876 |     if (!span) {
 877 |       continue;
 878 |     }
 879 |     const perfSpan = convertTraceSpanToPerformanceSpan(
 880 |       span,
 881 |       offenderSet,
 882 |       budget,
 883 |       0,
 884 |     );
 885 |     if (perfSpan) {
 886 |       roots.push(perfSpan);
 887 |     }
 888 |     if (budget.count >= budget.limit) {
 889 |       break;
 890 |     }
 891 |   }
 892 | 
 893 |   // If no parent spans found, try to find offender spans directly
 894 |   if (roots.length === 0 && offenderSpanIds.length > 0) {
 895 |     for (const offenderId of offenderSpanIds) {
 896 |       const span = spanMap.get(offenderId);
 897 |       if (!span) {
 898 |         continue;
 899 |       }
 900 |       const perfSpan = convertTraceSpanToPerformanceSpan(
 901 |         span,
 902 |         offenderSet,
 903 |         budget,
 904 |         0,
 905 |       );
 906 |       if (perfSpan) {
 907 |         roots.push(perfSpan);
 908 |       }
 909 |       if (budget.count >= budget.limit) {
 910 |         break;
 911 |       }
 912 |     }
 913 |   }
 914 | 
 915 |   if (roots.length === 0) {
 916 |     return [];
 917 |   }
 918 | 
 919 |   return renderPerformanceSpanTree(roots);
 920 | }
 921 | 
 922 | function convertTraceSpanToPerformanceSpan(
 923 |   span: TraceSpan,
 924 |   offenderSet: Set<string>,
 925 |   budget: { count: number; limit: number },
 926 |   level: number,
 927 | ): PerformanceSpan | null {
 928 |   if (budget.count >= budget.limit) {
 929 |     return null;
 930 |   }
 931 | 
 932 |   budget.count += 1;
 933 | 
 934 |   // Get span ID from additional_attributes or fall back to event_id
 935 |   const spanId =
 936 |     normalizeSpanId(span.additional_attributes?.span_id) || span.event_id;
 937 | 
 938 |   const performanceSpan: PerformanceSpan = {
 939 |     span_id: spanId,
 940 |     op: span.op || "unknown",
 941 |     description: formatTraceSpanDescription(span),
 942 |     duration: getTraceSpanDurationMs(span),
 943 |     is_n1_query: offenderSet.has(spanId),
 944 |     children: [],
 945 |     level,
 946 |   };
 947 | 
 948 |   for (const child of span.children ?? []) {
 949 |     if (!isTraceSpan(child)) {
 950 |       continue;
 951 |     }
 952 |     if (budget.count >= budget.limit) {
 953 |       break;
 954 |     }
 955 |     const childSpan = convertTraceSpanToPerformanceSpan(
 956 |       child,
 957 |       offenderSet,
 958 |       budget,
 959 |       level + 1,
 960 |     );
 961 |     if (childSpan) {
 962 |       performanceSpan.children.push(childSpan);
 963 |     }
 964 |     if (budget.count >= budget.limit) {
 965 |       break;
 966 |     }
 967 |   }
 968 | 
 969 |   return performanceSpan;
 970 | }
 971 | 
 972 | function formatTraceSpanDescription(span: TraceSpan): string {
 973 |   if (span.name && span.name.trim().length > 0) {
 974 |     return span.name.trim();
 975 |   }
 976 |   if (span.description && span.description.trim().length > 0) {
 977 |     return span.description.trim();
 978 |   }
 979 |   if (span.op && span.op.trim().length > 0) {
 980 |     return span.op.trim();
 981 |   }
 982 |   return "unnamed";
 983 | }
 984 | 
 985 | function getTraceSpanDurationMs(span: TraceSpan): number {
 986 |   if (typeof span.duration === "number" && span.duration >= 0) {
 987 |     return span.duration;
 988 |   }
 989 |   if (
 990 |     typeof (span as { end_timestamp?: number }).end_timestamp === "number" &&
 991 |     typeof span.start_timestamp === "number"
 992 |   ) {
 993 |     const deltaSeconds =
 994 |       (span as { end_timestamp: number }).end_timestamp - span.start_timestamp;
 995 |     if (Number.isFinite(deltaSeconds) && deltaSeconds >= 0) {
 996 |       return deltaSeconds * 1000;
 997 |     }
 998 |   }
 999 |   return 0;
1000 | }
1001 | 
1002 | function buildOffenderSummaries(
1003 |   spans: RawSpan[],
1004 |   offenderSpanIds: string[],
1005 | ): string[] {
1006 |   if (offenderSpanIds.length === 0) {
1007 |     return [];
1008 |   }
1009 | 
1010 |   const spanMap = new Map<string, RawSpan>();
1011 |   for (const span of spans) {
1012 |     const identifier = getSpanIdentifier(span);
1013 |     if (identifier) {
1014 |       spanMap.set(identifier, span);
1015 |     }
1016 |   }
1017 | 
1018 |   const summaries: string[] = [];
1019 |   for (const offenderId of offenderSpanIds) {
1020 |     const span = spanMap.get(offenderId);
1021 |     if (span) {
1022 |       const description = span.description || span.op || `Span ${offenderId}`;
1023 |       const duration = getSpanDurationMs(span);
1024 |       const durationLabel = duration > 0 ? ` (${Math.round(duration)}ms)` : "";
1025 |       summaries.push(`${description}${durationLabel} [${offenderId}] [N+1]`);
1026 |     } else {
1027 |       summaries.push(`Span ${offenderId} [N+1]`);
1028 |     }
1029 |   }
1030 | 
1031 |   return summaries;
1032 | }
1033 | 
1034 | /**
1035 |  * Renders a hierarchical tree of performance spans using box-drawing characters.
1036 |  * Highlights N+1 queries with a special indicator.
1037 |  *
1038 |  * @param spans - Array of selected performance spans
1039 |  * @returns Array of formatted strings representing the tree
1040 |  */
1041 | function renderPerformanceSpanTree(spans: PerformanceSpan[]): string[] {
1042 |   const lines: string[] = [];
1043 | 
1044 |   function renderSpan(span: PerformanceSpan, prefix = "", isLast = true): void {
1045 |     const connector = prefix === "" ? "" : isLast ? "└─ " : "├─ ";
1046 | 
1047 |     const displayName = span.description?.trim() || span.op || "unnamed";
1048 |     const shortId = span.span_id ? span.span_id.substring(0, 8) : "unknown";
1049 |     const durationDisplay =
1050 |       span.duration > 0 ? `${Math.round(span.duration)}ms` : "unknown";
1051 | 
1052 |     const metadataParts: string[] = [shortId];
1053 |     if (span.op && span.op !== "default") {
1054 |       metadataParts.push(span.op);
1055 |     }
1056 |     metadataParts.push(durationDisplay);
1057 | 
1058 |     const line = `${prefix}${connector}${displayName} [${metadataParts.join(
1059 |       " · ",
1060 |     )}]${span.is_n1_query ? " [N+1]" : ""}`;
1061 |     lines.push(line);
1062 | 
1063 |     // Render children
1064 |     for (let i = 0; i < span.children.length; i++) {
1065 |       const child = span.children[i];
1066 |       const isLastChild = i === span.children.length - 1;
1067 |       const childPrefix = prefix + (isLast ? "   " : "│  ");
1068 |       renderSpan(child, childPrefix, isLastChild);
1069 |     }
1070 |   }
1071 | 
1072 |   for (let i = 0; i < spans.length; i++) {
1073 |     const span = spans[i];
1074 |     const isLastRoot = i === spans.length - 1;
1075 |     renderSpan(span, "", isLastRoot);
1076 |   }
1077 | 
1078 |   return lines;
1079 | }
1080 | 
1081 | function selectN1QuerySpans(
1082 |   spans: RawSpan[],
1083 |   evidence: N1EvidenceData,
1084 |   maxSpans = MAX_SPANS_IN_TREE,
1085 | ): PerformanceSpan[] {
1086 |   const selected: PerformanceSpan[] = [];
1087 |   let spanCount = 0;
1088 | 
1089 |   const offenderSpanIds = normalizeIdArray(evidence.offenderSpanIds);
1090 |   const parentSpanIds = normalizeIdArray(evidence.parentSpanIds);
1091 | 
1092 |   let parentSpan: PerformanceSpan | null = null;
1093 |   if (parentSpanIds.length > 0) {
1094 |     const parent = spans.find((span) => {
1095 |       const identifier = getSpanIdentifier(span);
1096 |       return identifier ? parentSpanIds.includes(identifier) : false;
1097 |     });
1098 | 
1099 |     if (parent) {
1100 |       parentSpan = {
1101 |         span_id: getSpanIdentifier(parent) ?? "unknown",
1102 |         op: parent.op || "unknown",
1103 |         description:
1104 |           parent.description || evidence.parentSpan || "Parent Operation",
1105 |         duration: getSpanDurationMs(parent),
1106 |         is_n1_query: false,
1107 |         children: [],
1108 |         level: 0,
1109 |       };
1110 |       selected.push(parentSpan);
1111 |       spanCount += 1;
1112 |     }
1113 |   }
1114 | 
1115 |   if (offenderSpanIds.length > 0) {
1116 |     const offenderSet = new Set(offenderSpanIds);
1117 |     const offenderSpans = spans
1118 |       .filter((span) => {
1119 |         const identifier = getSpanIdentifier(span);
1120 |         return identifier ? offenderSet.has(identifier) : false;
1121 |       })
1122 |       .slice(0, Math.max(0, maxSpans - spanCount));
1123 | 
1124 |     for (const span of offenderSpans) {
1125 |       const perfSpan: PerformanceSpan = {
1126 |         span_id: getSpanIdentifier(span) ?? "unknown",
1127 |         op: span.op || "db.query",
1128 |         description: span.description || "Database Query",
1129 |         duration: getSpanDurationMs(span),
1130 |         is_n1_query: true,
1131 |         children: [],
1132 |         level: parentSpan ? 1 : 0,
1133 |       };
1134 | 
1135 |       if (parentSpan) {
1136 |         parentSpan.children.push(perfSpan);
1137 |       } else {
1138 |         selected.push(perfSpan);
1139 |       }
1140 | 
1141 |       spanCount += 1;
1142 |       if (spanCount >= maxSpans) {
1143 |         break;
1144 |       }
1145 |     }
1146 |   }
1147 | 
1148 |   return selected;
1149 | }
1150 | 
1151 | /**
1152 |  * Known Sentry performance issue types that we handle.
1153 |  *
1154 |  * NOTE: We intentionally only implement formatters for high-value performance issues
1155 |  * that provide complex insights. Not all issue types need custom formatting - many
1156 |  * can rely on the generic evidenceDisplay fields that Sentry provides.
1157 |  *
1158 |  * Currently fully implemented:
1159 |  * - N+1 query detection (DB and API)
1160 |  *
1161 |  * Partially implemented:
1162 |  * - Slow DB queries (shows parent span only)
1163 |  *
1164 |  * Not implemented (lower priority):
1165 |  * - Asset-related issues (render blocking, uncompressed, large payloads)
1166 |  * - File I/O issues
1167 |  * - Consecutive queries
1168 |  */
1169 | const KNOWN_PERFORMANCE_ISSUE_TYPES = {
1170 |   N_PLUS_ONE_DB_QUERIES: "performance_n_plus_one_db_queries",
1171 |   N_PLUS_ONE_API_CALLS: "performance_n_plus_one_api_calls",
1172 |   SLOW_DB_QUERY: "performance_slow_db_query",
1173 |   RENDER_BLOCKING_ASSET: "performance_render_blocking_asset",
1174 |   CONSECUTIVE_DB_QUERIES: "performance_consecutive_db_queries",
1175 |   FILE_IO_MAIN_THREAD: "performance_file_io_main_thread",
1176 |   M_N_PLUS_ONE_DB_QUERIES: "performance_m_n_plus_one_db_queries",
1177 |   UNCOMPRESSED_ASSET: "performance_uncompressed_asset",
1178 |   LARGE_HTTP_PAYLOAD: "performance_large_http_payload",
1179 | } as const;
1180 | 
1181 | /**
1182 |  * Map numeric occurrence types to issue types (from Sentry's codebase).
1183 |  *
1184 |  * Sentry uses numeric type IDs internally in the occurrence data structure,
1185 |  * but string issue types in the UI and other APIs. This mapping converts
1186 |  * between them.
1187 |  *
1188 |  * Source: sentry/static/app/types/group.tsx in Sentry's codebase
1189 |  * Range: 1xxx = transaction-based performance issues
1190 |  *        2xxx = profile-based performance issues
1191 |  */
1192 | const OCCURRENCE_TYPE_TO_ISSUE_TYPE: Record<number, string> = {
1193 |   1001: KNOWN_PERFORMANCE_ISSUE_TYPES.SLOW_DB_QUERY,
1194 |   1004: KNOWN_PERFORMANCE_ISSUE_TYPES.RENDER_BLOCKING_ASSET,
1195 |   1006: KNOWN_PERFORMANCE_ISSUE_TYPES.N_PLUS_ONE_DB_QUERIES,
1196 |   1906: KNOWN_PERFORMANCE_ISSUE_TYPES.N_PLUS_ONE_DB_QUERIES, // Alternative ID for N+1 DB
1197 |   1007: KNOWN_PERFORMANCE_ISSUE_TYPES.CONSECUTIVE_DB_QUERIES,
1198 |   1008: KNOWN_PERFORMANCE_ISSUE_TYPES.FILE_IO_MAIN_THREAD,
1199 |   1009: "performance_consecutive_http",
1200 |   1010: KNOWN_PERFORMANCE_ISSUE_TYPES.N_PLUS_ONE_API_CALLS,
1201 |   1910: KNOWN_PERFORMANCE_ISSUE_TYPES.N_PLUS_ONE_API_CALLS, // Alternative ID for N+1 API
1202 |   1012: KNOWN_PERFORMANCE_ISSUE_TYPES.UNCOMPRESSED_ASSET,
1203 |   1013: "performance_db_main_thread",
1204 |   1015: KNOWN_PERFORMANCE_ISSUE_TYPES.LARGE_HTTP_PAYLOAD,
1205 |   1016: "performance_http_overhead",
1206 | };
1207 | 
1208 | // Type alias currently unused but kept for potential future type safety
1209 | // type PerformanceIssueType = typeof KNOWN_PERFORMANCE_ISSUE_TYPES[keyof typeof KNOWN_PERFORMANCE_ISSUE_TYPES];
1210 | 
1211 | /**
1212 |  * Formats N+1 query issue evidence data.
1213 |  *
1214 |  * N+1 queries are a common performance anti-pattern where code executes
1215 |  * 1 query to get a list of items, then N additional queries (one per item)
1216 |  * instead of using a single JOIN or batch query.
1217 |  *
1218 |  * Evidence fields we use:
1219 |  * - parentSpan: The operation that triggered the N+1 queries
1220 |  * - repeatingSpansCompact/repeatingSpans: The query pattern being repeated
1221 |  * - numberRepeatingSpans: How many times the query was executed
1222 |  * - offenderSpanIds: IDs of the actual span instances
1223 |  * - parentSpanIds: IDs of parent spans for tree visualization
1224 |  */
1225 | function formatN1QueryEvidence(
1226 |   evidenceData: N1EvidenceData,
1227 |   spansData: unknown,
1228 |   performanceTrace?: Trace,
1229 | ): string {
1230 |   const parts: string[] = [];
1231 | 
1232 |   // Format parent span info if available
1233 |   if (evidenceData.parentSpan) {
1234 |     parts.push("**Parent Operation:**");
1235 |     parts.push(`${evidenceData.parentSpan}`);
1236 |     parts.push("");
1237 |   }
1238 | 
1239 |   // Format repeating spans (the N+1 queries)
1240 |   const repeatingLines = getRepeatingSpanLines(evidenceData);
1241 |   if (repeatingLines.length > 0) {
1242 |     parts.push("### Repeated Database Queries");
1243 |     parts.push("");
1244 | 
1245 |     const queryCount = evidenceData.numberRepeatingSpans
1246 |       ? safeParseInt(evidenceData.numberRepeatingSpans, 0)
1247 |       : evidenceData.numPatternRepetitions ||
1248 |         evidenceData.offenderSpanIds?.length ||
1249 |         0;
1250 | 
1251 |     if (queryCount > 0) {
1252 |       parts.push(`**Query executed ${queryCount} times:**`);
1253 |     }
1254 | 
1255 |     // Show the query pattern - if single line, render as SQL block; if multiple, as list
1256 |     if (repeatingLines.length === 1) {
1257 |       parts.push("```sql");
1258 |       parts.push(repeatingLines[0]);
1259 |       parts.push("```");
1260 |       parts.push("");
1261 |     } else {
1262 |       parts.push("**Repeated operations:**");
1263 |       for (const line of repeatingLines) {
1264 |         parts.push(`- ${line}`);
1265 |       }
1266 |       parts.push("");
1267 |     }
1268 |   }
1269 | 
1270 |   const parentSpanIds = normalizeIdArray(evidenceData.parentSpanIds);
1271 |   const offenderSpanIds = normalizeIdArray(evidenceData.offenderSpanIds);
1272 | 
1273 |   const traceLines = performanceTrace
1274 |     ? buildTraceSpanTree(
1275 |         performanceTrace,
1276 |         parentSpanIds,
1277 |         offenderSpanIds,
1278 |         MAX_SPANS_IN_TREE,
1279 |       )
1280 |     : [];
1281 | 
1282 |   if (traceLines.length > 0) {
1283 |     parts.push(`### Span Tree (Limited to ${MAX_SPANS_IN_TREE} spans)`);
1284 |     parts.push("");
1285 |     parts.push("```");
1286 |     parts.push(...traceLines);
1287 |     parts.push("```");
1288 |     parts.push("");
1289 |   } else {
1290 |     const spanTree = isValidSpanArray(spansData)
1291 |       ? selectN1QuerySpans(spansData, evidenceData, MAX_SPANS_IN_TREE)
1292 |       : [];
1293 | 
1294 |     if (spanTree.length > 0) {
1295 |       parts.push(`### Span Tree (Limited to ${MAX_SPANS_IN_TREE} spans)`);
1296 |       parts.push("");
1297 |       parts.push("```");
1298 |       parts.push(...renderPerformanceSpanTree(spanTree));
1299 |       parts.push("```");
1300 |       parts.push("");
1301 |     } else if (isValidSpanArray(spansData)) {
1302 |       // Only show offender summaries if we have spans data but couldn't build a tree
1303 |       const offenderSummaries = buildOffenderSummaries(
1304 |         spansData as RawSpan[],
1305 |         offenderSpanIds,
1306 |       );
1307 | 
1308 |       if (offenderSummaries.length > 0) {
1309 |         parts.push("### Offending Spans");
1310 |         parts.push("");
1311 |         for (const summary of offenderSummaries) {
1312 |           parts.push(`- ${summary}`);
1313 |         }
1314 |       }
1315 |     }
1316 |   }
1317 | 
1318 |   return parts.join("\n");
1319 | }
1320 | 
1321 | /**
1322 |  * Formats slow DB query issue evidence data.
1323 |  *
1324 |  * Currently only partially implemented - shows parent span information.
1325 |  * Full implementation would show query duration, explain plan, etc.
1326 |  *
1327 |  * This is lower priority as the generic evidenceDisplay fields usually
1328 |  * provide sufficient information for slow query issues.
1329 |  */
1330 | function formatSlowDbQueryEvidence(
1331 |   evidenceData: SlowDbEvidenceData,
1332 |   spansData: unknown,
1333 | ): string {
1334 |   const parts: string[] = [];
1335 | 
1336 |   // Show parent span if available (generic field that applies to slow queries)
1337 |   if (evidenceData.parentSpan) {
1338 |     parts.push("**Parent Operation:**");
1339 |     parts.push(`${evidenceData.parentSpan}`);
1340 |     parts.push("");
1341 |   }
1342 | 
1343 |   // TODO: Implement slow query specific fields when we know the structure
1344 |   // Potential fields: query duration, database name, query plan
1345 |   console.warn(
1346 |     "[formatSlowDbQueryEvidence] Evidence data rendering not yet fully implemented",
1347 |   );
1348 | 
1349 |   return parts.join("\n");
1350 | }
1351 | 
1352 | /**
1353 |  * Formats performance issue details from transaction events based on the issue type.
1354 |  *
1355 |  * This is the main dispatcher for performance issue formatting. It:
1356 |  * 1. Detects the issue type from occurrence data (numeric or string)
1357 |  * 2. Calls the appropriate type-specific formatter if implemented
1358 |  * 3. Falls back to generic evidenceDisplay fields for unimplemented types
1359 |  * 4. Provides span analysis fallback for events without occurrence data
1360 |  *
1361 |  * The occurrence data structure comes from Sentry's performance issue detection
1362 |  * and contains evidence about what triggered the issue.
1363 |  *
1364 |  * @param event - The transaction event containing performance issue data
1365 |  * @param spansData - The spans data from the event entries
1366 |  * @returns Formatted markdown string with performance issue details
1367 |  */
1368 | function formatPerformanceIssueOutput(
1369 |   event: Event,
1370 |   spansData: unknown,
1371 |   options?: {
1372 |     performanceTrace?: Trace;
1373 |   },
1374 | ): string {
1375 |   const parts: string[] = [];
1376 | 
1377 |   // Check if we have occurrence data
1378 |   const occurrence = (event as any).occurrence;
1379 |   if (!occurrence) {
1380 |     return "";
1381 |   }
1382 | 
1383 |   // Get issue type - occurrence.type is numeric, issueType may be a string
1384 |   let issueType: string | undefined;
1385 |   if (typeof occurrence.type === "number") {
1386 |     issueType = OCCURRENCE_TYPE_TO_ISSUE_TYPE[occurrence.type];
1387 |   } else {
1388 |     issueType = occurrence.issueType || occurrence.type;
1389 |   }
1390 | 
1391 |   const evidenceData = occurrence.evidenceData;
1392 | 
1393 |   // Process evidence data based on known performance issue types
1394 |   if (evidenceData) {
1395 |     switch (issueType) {
1396 |       case KNOWN_PERFORMANCE_ISSUE_TYPES.N_PLUS_ONE_DB_QUERIES:
1397 |       case KNOWN_PERFORMANCE_ISSUE_TYPES.N_PLUS_ONE_API_CALLS:
1398 |       case KNOWN_PERFORMANCE_ISSUE_TYPES.M_N_PLUS_ONE_DB_QUERIES: {
1399 |         const result = formatN1QueryEvidence(
1400 |           evidenceData,
1401 |           spansData,
1402 |           options?.performanceTrace,
1403 |         );
1404 |         if (result) parts.push(result);
1405 |         break;
1406 |       }
1407 | 
1408 |       case KNOWN_PERFORMANCE_ISSUE_TYPES.SLOW_DB_QUERY: {
1409 |         const result = formatSlowDbQueryEvidence(evidenceData, spansData);
1410 |         if (result) parts.push(result);
1411 |         break;
1412 |       }
1413 | 
1414 |       default:
1415 |         // We don't implement formatters for all performance issue types.
1416 |         // Many lower-priority issues (consecutive queries, asset issues, file I/O)
1417 |         // work fine with just the generic evidenceDisplay fields below.
1418 |         // Only high-value, complex issues like N+1 queries need custom formatting.
1419 |         if (issueType) {
1420 |           console.warn(
1421 |             `[formatPerformanceIssueOutput] No custom formatter for issue type: ${issueType}`,
1422 |           );
1423 |         }
1424 |       // Fall through to show generic evidence display below
1425 |     }
1426 |   }
1427 | 
1428 |   // Show transaction name if available for any performance issue (generic field)
1429 |   if (evidenceData?.transactionName) {
1430 |     parts.push("**Transaction:**");
1431 |     parts.push(`${evidenceData.transactionName}`);
1432 |     parts.push("");
1433 |   }
1434 | 
1435 |   // Always show evidence display if available (this is generic and doesn't require type knowledge)
1436 |   if (occurrence.evidenceDisplay?.length > 0) {
1437 |     for (const display of occurrence.evidenceDisplay) {
1438 |       if (display.important) {
1439 |         parts.push(`**${display.name}:**`);
1440 |         parts.push(`${display.value}`);
1441 |         parts.push("");
1442 |       }
1443 |     }
1444 |   }
1445 | 
1446 |   return parts.length > 0 ? `${parts.join("\n")}\n` : "";
1447 | }
1448 | 
1449 | /**
1450 |  * Formats generic event output (performance regressions, metric-based issues).
1451 |  * Generic events don't have traditional error entries, but have occurrence data
1452 |  * with evidenceDisplay showing regression details, metric changes, etc.
1453 |  */
1454 | function formatGenericEventOutput(event: Event): string {
1455 |   const parts: string[] = [];
1456 | 
1457 |   // Only generic events have occurrence data
1458 |   if (event.type !== "generic") {
1459 |     return "";
1460 |   }
1461 | 
1462 |   // Type assertion after guard - we know it's a GenericEvent
1463 |   const genericEvent = event as GenericEvent;
1464 |   const occurrence = genericEvent.occurrence;
1465 |   if (!occurrence) {
1466 |     return "";
1467 |   }
1468 | 
1469 |   // Add a section header for performance regression details
1470 |   const evidenceData = occurrence.evidenceData;
1471 |   if (evidenceData) {
1472 |     parts.push("### Performance Regression Details");
1473 |     parts.push("");
1474 |   }
1475 | 
1476 |   // Show evidence display items (regression details, metric changes, etc.)
1477 |   if (occurrence.evidenceDisplay && occurrence.evidenceDisplay.length > 0) {
1478 |     for (const display of occurrence.evidenceDisplay) {
1479 |       if (display.important) {
1480 |         parts.push(`**${display.name}:**`);
1481 |         parts.push(`${display.value}`);
1482 |         parts.push("");
1483 |       }
1484 |     }
1485 |   }
1486 | 
1487 |   return parts.length > 0 ? `${parts.join("\n")}\n` : "";
1488 | }
1489 | 
1490 | function formatTags(tags: z.infer<typeof EventSchema>["tags"]) {
1491 |   if (!tags || tags.length === 0) {
1492 |     return "";
1493 |   }
1494 |   return `### Tags\n\n${tags
1495 |     .map((tag) => `**${tag.key}**: ${tag.value}`)
1496 |     .join("\n")}\n\n`;
1497 | }
1498 | 
1499 | function formatContext(context: z.infer<typeof EventSchema>["context"]) {
1500 |   if (!context || Object.keys(context).length === 0) {
1501 |     return "";
1502 |   }
1503 |   return `### Extra Data\n\nAdditional data attached to this event.\n\n${Object.entries(
1504 |     context,
1505 |   )
1506 |     .map(([key, value]) => {
1507 |       return `**${key}**: ${JSON.stringify(value, undefined, 2)}`;
1508 |     })
1509 |     .join("\n")}\n\n`;
1510 | }
1511 | 
1512 | function formatContexts(contexts: z.infer<typeof EventSchema>["contexts"]) {
1513 |   if (!contexts || Object.keys(contexts).length === 0) {
1514 |     return "";
1515 |   }
1516 |   return `### Additional Context\n\nThese are additional context provided by the user when they're instrumenting their application.\n\n${Object.entries(
1517 |     contexts,
1518 |   )
1519 |     .map(
1520 |       ([name, data]) =>
1521 |         `**${name}**\n${Object.entries(data)
1522 |           .filter(([key, _]) => key !== "type")
1523 |           .map(([key, value]) => {
1524 |             return `${key}: ${JSON.stringify(value, undefined, 2)}`;
1525 |           })
1526 |           .join("\n")}`,
1527 |     )
1528 |     .join("\n\n")}\n\n`;
1529 | }
1530 | 
1531 | /**
1532 |  * Formats a brief Seer analysis summary for inclusion in issue details.
1533 |  * Shows current status and high-level insights, prompting to use analyze_issue_with_seer for full details.
1534 |  *
1535 |  * @param autofixState - The autofix state containing Seer analysis data
1536 |  * @param organizationSlug - The organization slug for the issue
1537 |  * @param issueId - The issue ID (shortId)
1538 |  * @returns Formatted markdown string with Seer summary, or empty string if no analysis exists
1539 |  */
1540 | function formatSeerSummary(
1541 |   autofixState: AutofixRunState | undefined,
1542 |   organizationSlug: string,
1543 |   issueId: string,
1544 | ): string {
1545 |   if (!autofixState || !autofixState.autofix) {
1546 |     return "";
1547 |   }
1548 | 
1549 |   const { autofix } = autofixState;
1550 |   const parts: string[] = [];
1551 | 
1552 |   parts.push("## Seer Analysis");
1553 |   parts.push("");
1554 | 
1555 |   // Show status first
1556 |   const statusDisplay = getStatusDisplayName(autofix.status);
1557 |   if (!isTerminalStatus(autofix.status)) {
1558 |     parts.push(`**Status:** ${statusDisplay}`);
1559 |     parts.push("");
1560 |   }
1561 | 
1562 |   // Show summary of what we have so far
1563 |   if (autofix.steps.length > 0) {
1564 |     const completedSteps = autofix.steps.filter(
1565 |       (step) => step.status === "COMPLETED",
1566 |     );
1567 | 
1568 |     // Find the solution step if available
1569 |     const solutionStep = completedSteps.find(
1570 |       (step) => step.type === "solution",
1571 |     );
1572 | 
1573 |     if (solutionStep) {
1574 |       // For solution steps, use the description directly
1575 |       const solutionDescription = solutionStep.description;
1576 |       if (
1577 |         solutionDescription &&
1578 |         typeof solutionDescription === "string" &&
1579 |         solutionDescription.trim()
1580 |       ) {
1581 |         parts.push("**Summary:**");
1582 |         parts.push(solutionDescription.trim());
1583 |       } else {
1584 |         // Fallback to extracting from output if no description
1585 |         const solutionOutput = getOutputForAutofixStep(solutionStep);
1586 |         const lines = solutionOutput.split("\n");
1587 |         const firstParagraph = lines.find(
1588 |           (line) =>
1589 |             line.trim().length > 50 &&
1590 |             !line.startsWith("#") &&
1591 |             !line.startsWith("*"),
1592 |         );
1593 |         if (firstParagraph) {
1594 |           parts.push("**Summary:**");
1595 |           parts.push(firstParagraph.trim());
1596 |         }
1597 |       }
1598 |     } else if (completedSteps.length > 0) {
1599 |       // Show what steps have been completed so far
1600 |       const rootCauseStep = completedSteps.find(
1601 |         (step) => step.type === "root_cause_analysis",
1602 |       );
1603 | 
1604 |       if (rootCauseStep) {
1605 |         const typedStep = rootCauseStep as z.infer<
1606 |           typeof AutofixRunStepRootCauseAnalysisSchema
1607 |         >;
1608 |         if (
1609 |           typedStep.causes &&
1610 |           typedStep.causes.length > 0 &&
1611 |           typedStep.causes[0].description
1612 |         ) {
1613 |           parts.push("**Root Cause Identified:**");
1614 |           parts.push(typedStep.causes[0].description.trim());
1615 |         }
1616 |       } else {
1617 |         // Show generic progress
1618 |         parts.push(
1619 |           `**Progress:** ${completedSteps.length} of ${autofix.steps.length} steps completed`,
1620 |         );
1621 |       }
1622 |     }
1623 |   } else {
1624 |     // No steps yet - check for terminal states first
1625 |     if (isTerminalStatus(autofix.status)) {
1626 |       if (autofix.status === "FAILED" || autofix.status === "ERROR") {
1627 |         parts.push("**Status:** Analysis failed.");
1628 |       } else if (autofix.status === "CANCELLED") {
1629 |         parts.push("**Status:** Analysis was cancelled.");
1630 |       } else if (
1631 |         autofix.status === "NEED_MORE_INFORMATION" ||
1632 |         autofix.status === "WAITING_FOR_USER_RESPONSE"
1633 |       ) {
1634 |         parts.push(
1635 |           "**Status:** Analysis paused - additional information needed.",
1636 |         );
1637 |       }
1638 |     } else {
1639 |       parts.push("Analysis has started but no results yet.");
1640 |     }
1641 |   }
1642 | 
1643 |   // Add specific messages for terminal states when steps exist
1644 |   if (autofix.steps.length > 0 && isTerminalStatus(autofix.status)) {
1645 |     if (autofix.status === "FAILED" || autofix.status === "ERROR") {
1646 |       parts.push("");
1647 |       parts.push("**Status:** Analysis failed.");
1648 |     } else if (autofix.status === "CANCELLED") {
1649 |       parts.push("");
1650 |       parts.push("**Status:** Analysis was cancelled.");
1651 |     } else if (
1652 |       autofix.status === "NEED_MORE_INFORMATION" ||
1653 |       autofix.status === "WAITING_FOR_USER_RESPONSE"
1654 |     ) {
1655 |       parts.push("");
1656 |       parts.push(
1657 |         "**Status:** Analysis paused - additional information needed.",
1658 |       );
1659 |     }
1660 |   }
1661 | 
1662 |   // Always suggest using analyze_issue_with_seer for more details
1663 |   parts.push("");
1664 |   parts.push(
1665 |     `**Note:** For detailed root cause analysis and solutions, call \`analyze_issue_with_seer(organizationSlug='${organizationSlug}', issueId='${issueId}')\``,
1666 |   );
1667 | 
1668 |   return `${parts.join("\n")}\n\n`;
1669 | }
1670 | 
1671 | /**
1672 |  * Formats a Sentry issue with its latest event into comprehensive markdown output.
1673 |  * Includes issue metadata, event details, and usage instructions.
1674 |  *
1675 |  * @param params - Object containing organization slug, issue, event, and API service
1676 |  * @returns Formatted markdown string with complete issue information
1677 |  */
1678 | export function formatIssueOutput({
1679 |   organizationSlug,
1680 |   issue,
1681 |   event,
1682 |   apiService,
1683 |   autofixState,
1684 |   performanceTrace,
1685 | }: {
1686 |   organizationSlug: string;
1687 |   issue: Issue;
1688 |   event: Event;
1689 |   apiService: SentryApiService;
1690 |   autofixState?: AutofixRunState;
1691 |   performanceTrace?: Trace;
1692 | }) {
1693 |   let output = `# Issue ${issue.shortId} in **${organizationSlug}**\n\n`;
1694 | 
1695 |   // Check if this is a performance issue based on issueCategory or issueType
1696 |   // Performance issues can have various categories like 'db_query' but issueType starts with 'performance_'
1697 |   const isPerformanceIssue =
1698 |     issue.issueType?.startsWith("performance_") ||
1699 |     issue.issueCategory === "performance";
1700 | 
1701 |   if (isPerformanceIssue && issue.metadata) {
1702 |     // For performance issues, use metadata for better context
1703 |     const issueTitle = issue.metadata.title || issue.title;
1704 |     output += `**Description**: ${issueTitle}\n`;
1705 | 
1706 |     if (issue.metadata.location) {
1707 |       output += `**Location**: ${issue.metadata.location}\n`;
1708 |     }
1709 |     if (issue.metadata.value) {
1710 |       output += `**Query Pattern**: \`${issue.metadata.value}\`\n`;
1711 |     }
1712 |   } else {
1713 |     // For regular errors and other issues
1714 |     output += `**Description**: ${issue.title}\n`;
1715 |     output += `**Culprit**: ${issue.culprit}\n`;
1716 |   }
1717 | 
1718 |   output += `**First Seen**: ${new Date(issue.firstSeen).toISOString()}\n`;
1719 |   output += `**Last Seen**: ${new Date(issue.lastSeen).toISOString()}\n`;
1720 |   output += `**Occurrences**: ${issue.count}\n`;
1721 |   output += `**Users Impacted**: ${issue.userCount}\n`;
1722 |   output += `**Status**: ${issue.status}\n`;
1723 | 
1724 |   // Add substatus if present (e.g., "regressed" for performance regressions)
1725 |   if (issue.substatus) {
1726 |     output += `**Substatus**: ${issue.substatus}\n`;
1727 |   }
1728 | 
1729 |   // Add assignee information if assigned
1730 |   if (issue.assignedTo) {
1731 |     if (typeof issue.assignedTo === "string") {
1732 |       output += `**Assigned To**: ${issue.assignedTo}\n`;
1733 |     } else {
1734 |       const assignee = issue.assignedTo;
1735 |       const type = assignee.type === "team" ? "Team" : "User";
1736 |       output += `**Assigned To**: ${assignee.name} (${type})\n`;
1737 |     }
1738 |   }
1739 | 
1740 |   // Add issue type and category for performance/metric issues
1741 |   if (issue.issueType) {
1742 |     output += `**Issue Type**: ${issue.issueType}\n`;
1743 |   }
1744 |   if (issue.issueCategory) {
1745 |     output += `**Issue Category**: ${issue.issueCategory}\n`;
1746 |   }
1747 | 
1748 |   output += `**Platform**: ${issue.platform}\n`;
1749 |   output += `**Project**: ${issue.project.name}\n`;
1750 |   output += `**URL**: ${apiService.getIssueUrl(organizationSlug, issue.shortId)}\n`;
1751 |   output += "\n";
1752 |   output += "## Event Details\n\n";
1753 | 
1754 |   // Check if this is an unsupported event type
1755 |   // Event type union is: ErrorEvent | DefaultEvent | TransactionEvent | GenericEvent | CspEvent
1756 |   // But in practice we may have other types returned as UnknownEvent
1757 |   const eventType = event.type;
1758 |   const isUnsupportedType =
1759 |     eventType !== "error" &&
1760 |     eventType !== "default" &&
1761 |     eventType !== "transaction" &&
1762 |     eventType !== "generic" &&
1763 |     eventType !== "csp";
1764 | 
1765 |   if (isUnsupportedType) {
1766 |     // Log to Sentry for tracking new/unknown event types
1767 |     const sentryEventId = logIssue(
1768 |       `Unsupported event type encountered: ${String(eventType)}`,
1769 |       {
1770 |         contexts: {
1771 |           event: {
1772 |             event_id: event.id,
1773 |             event_type: eventType,
1774 |             issue_id: issue.id,
1775 |             issue_short_id: issue.shortId,
1776 |             organization_slug: organizationSlug,
1777 |             project_slug: issue.project.slug,
1778 |           },
1779 |         },
1780 |       },
1781 |     );
1782 | 
1783 |     output += `⚠️  **Warning**: Unsupported event type "${String(eventType)}"\n\n`;
1784 |     output += "This event type is not yet fully supported by the MCP server. ";
1785 |     output += "Only basic issue information is shown above.\n\n";
1786 |     output +=
1787 |       "**Please report this**: Open a GitHub issue at https://github.com/getsentry/sentry-mcp/issues/new ";
1788 |     output += `and include Event ID **${event.id}**`;
1789 |     if (sentryEventId) {
1790 |       output += ` and Sentry Event ID **${sentryEventId}**`;
1791 |     }
1792 |     output += " to help us add support for this event type.\n";
1793 | 
1794 |     // For unsupported event types, return early without trying to render event details
1795 |     return output;
1796 |   }
1797 | 
1798 |   output += `**Event ID**: ${event.id}\n`;
1799 |   output += `**Type**: ${event.type}\n`;
1800 |   // "default" type represents error events without exception data
1801 |   // "generic" type represents performance regressions and metric-based issues
1802 |   // "csp" type represents Content Security Policy violations
1803 |   if (
1804 |     event.type === "error" ||
1805 |     event.type === "default" ||
1806 |     event.type === "generic" ||
1807 |     event.type === "csp"
1808 |   ) {
1809 |     const typedEvent = event as
1810 |       | z.infer<typeof ErrorEventSchema>
1811 |       | z.infer<typeof DefaultEventSchema>
1812 |       | z.infer<typeof GenericEventSchema>
1813 |       | any; // CSP events don't have a schema yet
1814 |     if (typedEvent.dateCreated) {
1815 |       output += `**Occurred At**: ${new Date(typedEvent.dateCreated).toISOString()}\n`;
1816 |     }
1817 |   }
1818 |   if (event.message) {
1819 |     output += `**Message**:\n${event.message}\n`;
1820 |   }
1821 |   output += "\n";
1822 |   output += formatEventOutput(event, { performanceTrace });
1823 | 
1824 |   // Add Seer context if available
1825 |   if (autofixState) {
1826 |     output += formatSeerSummary(autofixState, organizationSlug, issue.shortId);
1827 |   }
1828 | 
1829 |   output += "# Using this information\n\n";
1830 |   output += `- You can reference the IssueID in commit messages (e.g. \`Fixes ${issue.shortId}\`) to automatically close the issue when the commit is merged.\n`;
1831 |   output +=
1832 |     "- The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.\n";
1833 |   return output;
1834 | }
1835 | 
```
Page 18/20FirstPrevNextLast