cmd / sentry

sentry is a Go CLI that inspects production Sentry issues from the terminal, so a backtrace can go straight to an agent without a trip through the web UI.

Run it with an issue's short or numeric ID:

go run ./cmd/sentry list           # unresolved production issues
go run ./cmd/sentry show <ID>      # latest stack trace for an issue
go run ./cmd/sentry job <ID>       # queue job and company context

It reads SENTRY_AUTH_TOKEN from the environment (or .env). The token needs event:read scope.

Two clients, opposite directions

The go/sentry article covers the runtime errs package: it uses DSN auth to send events to Sentry. This CLI reads from Sentry's web API using bearer-token auth against a different endpoint.

The read client lives in sentry/client.go and is shared with cmd/deploy, which uses it to tag releases. The same sentry.Client exposes release methods (project:releases scope) and issue-inspection methods (event:read scope). It follows the same HTTP client shape as go/render: functional options, a single request method, the shared backoff helper with 429 and 500 marked transient.

list

list queries is:unresolved environment:production and prints a tabwriter table of short ID, count, level, and title. It does not follow pagination; the first page covers the human-driven use case.

show

show prints an issue's latest event: metadata, tags, and the stack trace of each exception in the chain.

Sentry's events endpoints take a numeric ID, but the ID a human copies from the UI is a short one like EDS-4S0. So show resolves short IDs up front:

func resolveIssueID(ctx context.Context, c *sentry.Client, id string) (string, error) {
	if isShortID(id) {
		numeric, err := c.ResolveShortID(ctx, sentryOrg, id)
		if err != nil {
			return "", fmt.Errorf("resolve short id %q: %w", id, err)
		}
		return numeric, nil
	}
	return id, nil
}

func isShortID(id string) bool {
	return id != "" && id[0] >= 'A' && id[0] <= 'Z'
}

Sentry returns frames oldest-first (top of stack to the point of failure). show reverses them for a conventional newest-first traceback, the mirror of the ordering fix the errs client applies on the way out:

frames := exc.Stacktrace.Frames
for i := len(frames) - 1; i >= 0; i-- {
	f := frames[i]
	fmt.Printf("    %s:%d in %s\n", f.Filename, f.LineNo, f.Function)
}

job

job is the piece with no cmd/deploy equivalent. Most production errors come from the job queue, and every job event carries a job_id tag. job reads that tag off the latest event, then joins it against Postgres to print the queue job and, through its co_id, the company and domain:

jobID, err := strconv.ParseInt(ev.Tag("job_id"), 10, 64)
if err != nil {
	return fmt.Errorf("parse job_id %q: %w", ev.Tag("job_id"), err)
}
job, err := fetchJobByID(ctx, db, jobID)

That correlation turns "an error happened" into "this job for this customer, with these arguments, at this time." The DB queries live in queries/*.sql, embedded with go:embed.

Design

The Sentry coordinates (org, project) are hardcoded. eds is the only project this CLI inspects, so a flag would add ceremony for a value that never changes.

The event and issue structs model only the fields the CLI prints. Sentry's full event schema is large; a partial struct decodes what it needs and ignores the rest.

← All articles