Skip to content

feat(sentry): full Sentry.io integration#184

Merged
rafeegnash merged 5 commits into
masterfrom
feat/sentry-integration
Jun 1, 2026
Merged

feat(sentry): full Sentry.io integration#184
rafeegnash merged 5 commits into
masterfrom
feat/sentry-integration

Conversation

@rafeegnash
Copy link
Copy Markdown
Collaborator

Summary

Adds a first-class Sentry.io provider modeled on the existing Cloudflare integration. Headline feature: `clanker sentry ask "what's blowing up right now?"` — a natural-language agent that fetches issues, releases, and monitors on demand. Plus the full read/write surface for triage automation.

What lands

  • `internal/sentry/` — REST client (Bearer auth, cursor pagination via Link header, exponential backoff on 429 honoring `Retry-After` + `X-Sentry-Rate-Limit-Reset`), conversation history (per-org JSON at `~/.clanker/sentry-{slug}.json`), and per-resource modules: `issues`, `events`, `projects`, `releases`, `alerts`, `monitors`, `teams`, `members`, `orgs`.
  • `clanker sentry` cobra tree (`sn` alias): `list`, `get`, `resolve`, `ignore`, `assign`, `monitor mute|unmute|checkins`, `alert delete`. `--format json` for scripting.
  • `clanker sentry ask` — LLM-powered triage. Keyword routing picks which Sentry data to gather (issues / releases / monitors / alerts) and the search syntax `is:unresolved level:error environment:prod` is forwarded verbatim.
  • MCP tools: `clanker_sentry_ask`, `_list_issues`, `_get_issue`, `_resolve_issues`, `_list_releases`. Read-only tools are flagged accordingly.
  • Self-hosted support via configurable `sentry.host` (default `sentry.io`; also covers EU `.sentry.io`).
  • Config example in `.clanker.example.yaml` with scope guidance.

Decisions

  • Hit the REST API directly with `net/http` — no official Go management SDK (`getsentry/sentry-go` is for error reporting only).
  • IDs as `string` everywhere; ISO-8601 timestamps; `Event.Entries` polymorphic via `[]json.RawMessage`.
  • `ResolveIssues` / `IgnoreIssues` / `AssignIssue` use `PUT /organizations/{org}/issues/?id=A&id=B` — repeated key encoding, since BuildQuery flattens.
  • Releases stay read-only this round per scoping; alerts / monitors get full CRUD.

Test plan

  • `make ci` — full lint + vet + test + build clean
  • `go test -race -count=1 ./internal/sentry/...` — passes
  • Unit coverage: happy path, 429 retry-after, Link cursor parsing, error envelope detail extraction, repeated `?id=` query encoding, history round-trip + truncation
  • Smoke against a real Sentry token: `clanker sentry list issues --unresolved` returns rows
  • `clanker sentry ask "what's the worst error today?"` returns an LLM summary referencing fetched issues
  • MCP smoke: `clanker mcp --transport http --listen :39393` → `/tools/list` includes `clanker_sentry_*`

Out of scope

  • Release create/update (read-only this round)
  • OAuth (User Auth Token only)
  • Webhook receiver for live tail
  • Performance / spans / replays (future)

nash added 5 commits June 1, 2026 12:43
Adds a full Sentry provider mirroring the existing Cloudflare recipe:
internal/sentry package (REST client with cursor pagination, 429 backoff
honoring Retry-After + X-Sentry-Rate-Limit-Reset, per-org conversation
history) plus `clanker sentry` cobra tree (list / get / resolve / ignore
/ assign / monitor / alert subcommands) and `clanker sentry ask` for
natural-language triage. Self-hosted Sentry is supported via a
configurable host field (default sentry.io).

The ask agent fetches issues / releases / monitors / alerts based on
keyword routing and forwards Sentry search syntax verbatim — no
client-side parsing — so operators can use `is:unresolved level:error
environment:prod`-style queries directly.

MCP tools: clanker_sentry_ask, _list_issues, _get_issue,
_resolve_issues, _list_releases. Tests cover the happy path, 429
retry, Link-header cursor parsing, error envelope extraction,
PUT ?id=... repeated-key encoding, and per-org history round-trip.

Closes nothing — net-new integration.
Five fixes from a fresh-eyes review of the Sentry CLI:

- cmd/sentry.go: rename local `context` so it stops shadowing the
  imported context package — compiles today but trips up any future
  context.WithTimeout call placed below it.
- internal/sentry/issues.go: URL-escape ids in the ?id=A&id=B query of
  UpdateIssues via url.Values{}.Add. An id containing & or = (or a
  hostile MCP caller payload) could otherwise inject a status= param
  into the PUT.
- internal/sentry/conversation.go: introduce safeSlug() to strip
  anything outside [A-Za-z0-9_-] from the org slug before building
  ~/.clanker/sentry-{slug}.json — closes a path-traversal hole where
  `../../etc/passwd` would write outside ~/.clanker because
  filepath.Join cleans `..` segments.
- internal/sentry/static_commands.go: introduce sentryFlag() helper
  that reads via cmd.Flags() so persistent flags registered on the
  `sentry` command (not on rootCmd) are reachable from leaf subcommands
  three levels deep. Replaces every cmd.Root().PersistentFlags() call
  that silently returned "" at depth.
- renderJSON no longer pretends to honour --format — comment clarifies
  intent and `format` parameter is renamed to `_` to make it obvious
  to readers.

Test coverage:
- TestUpdateIssues_EscapesMaliciousID exercises an id containing
  `&status=resolved`, asserting it round-trips as a single escaped id
  and does NOT smuggle a second query parameter.
- TestSafeSlug_BlocksPathTraversal covers `../`, leading `/`, empty
  string, and dots-only inputs.

Docs: updated the Sentry scope recommendation in .clanker.example.yaml
to drop `project:releases` (an internal-integration-only scope) in
favour of `project:write`, which is the canonical User Auth Token
scope for release management.
Second-pass review surfaced an SSRF vector via the configured
sentry.host: NewClient accepted any value and built a Bearer-token
request against it, so a hostile sentry.host config or SENTRY_HOST env
var could redirect the auth token at 169.254.169.254 or other internal
endpoints.

- internal/sentry/client.go: introduce validateHost(). Rejects IP
  literals (catches 169.254.169.254, ::1, etc.), hostnames containing
  port/path/userinfo characters (`:` `/` `@` `?` `#`), and a small
  block-list of cloud-metadata DNS names (localhost,
  metadata.google.internal, instance-data). DNS hostnames matching
  `[a-z0-9]([a-z0-9\-]*[a-z0-9])?(\.…)*` continue to work for both
  sentry.io SaaS and self-hosted installs.
- internal/sentry/client_test.go: TestValidateHost_BlocksSSRF covers
  the allowed and blocked shapes; TestNewClient_RejectsHostileHost
  asserts the guard surfaces in the constructor, not silently.
- cmd/mcp_sentry.go: add WithReadOnlyHintAnnotation to
  clanker_sentry_ask. The tool only calls ListIssues / ListReleases /
  ListMonitors / ListIssueAlertRules then routes through the LLM —
  cautious MCP clients (Claude Desktop's safe-tool list) can now
  invoke it without user confirmation.
… trim narration

CLI cleanup driven by a third review round.

Perf:
- internal/sentry/status.go: run ListProjects + the two ListIssues calls
  concurrently via errgroup. The ask command's cold-start was dominated
  by these sequential network round-trips.
- cmd/sentry.go (gatherSentryContext): the up-to-4 selected sections
  (issues / releases / monitors / alerts) now fetch in parallel via
  errgroup, each writing into its own string block that gets stitched
  in fixed order. Worst-case ask latency drops ~4x.

Quality:
- internal/sentry/issues.go: introduce IssueStatus type + the three
  constants Sentry actually accepts. Status field on IssueUpdate is
  typed now, so a typo like "resolve" won't silently no-op.
- internal/sentry/static_commands.go: rename mustClient → buildClient.
  Go convention reserves must* for panics; this returns an error.
- internal/sentry/static_commands.go: drop the unused `format` param
  from renderJSON. get-resource output is JSON regardless of --format
  (varied per-resource shapes), so the symmetry-only parameter was
  misleading.
- Trim PR-review-style comments ("Earlier revisions used...", "Renamed
  from `context`...") that were useful in the original commit message
  but rot in the source.
@rafeegnash rafeegnash merged commit b6407c8 into master Jun 1, 2026
1 check passed
@rafeegnash rafeegnash deleted the feat/sentry-integration branch June 1, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant