// Sentry API helper // Fetches projects, issues, events, and org-level stats import { resolveCredential } from "./credentials"; import { requireRequestCredentialContext, scopedCredentialCacheKey, } from "./credentials-context"; const API_BASE = "https://sentry.io/api/0"; // In-memory cache const cache = new Map(); const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes const MAX_CACHE = 100; async function getToken(): Promise { const ctx = requireRequestCredentialContext("SENTRY_AUTH_TOKEN"); const token = (await resolveCredential("SENTRY_SERVER_TOKEN", ctx)) ?? (await resolveCredential("SENTRY_AUTH_TOKEN", ctx)); if (!token) throw new Error("SENTRY_AUTH_TOKEN not configured"); return token; } async function getOrgSlug(orgSlug?: string): Promise { const trimmed = orgSlug?.trim(); if (trimmed) return trimmed; const ctx = requireRequestCredentialContext("SENTRY_AUTH_TOKEN"); const configured = await resolveCredential("SENTRY_ORG_SLUG", ctx); if (configured) return configured; const organizations = await listOrganizations(); const discovered = organizations[0]?.slug; if (discovered) return discovered; throw new Error( "SENTRY_ORG_SLUG not configured and no accessible Sentry organizations found. Pass --orgSlug or configure SENTRY_ORG_SLUG.", ); } function cacheSet(key: string, data: unknown) { if (cache.size >= MAX_CACHE) { const oldest = cache.keys().next().value; if (oldest) cache.delete(oldest); } cache.set(key, { data, ts: Date.now() }); } async function apiGet(path: string, cacheKey?: string): Promise { const key = scopedCredentialCacheKey(cacheKey ?? path, "SENTRY_SERVER_TOKEN"); const cached = cache.get(key); if (cached && Date.now() - cached.ts < CACHE_TTL_MS) { return cached.data as T; } const res = await fetch(`${API_BASE}${path}`, { headers: { Authorization: `Bearer ${await getToken()}`, }, }); if (!res.ok) { const text = await res.text(); throw new Error(`Sentry API error ${res.status}: ${text}`); } const data = await res.json(); cacheSet(key, data); return data as T; } // -- Types -- export interface SentryProject { id: string; slug: string; name: string; platform: string | null; dateCreated: string; isBookmarked: boolean; isMember: boolean; hasAccess: boolean; status: string; } export interface SentryOrganization { id: string; slug: string; name: string; status?: { id?: string; name?: string }; dateCreated?: string; } export interface SentryIssue { id: string; shortId: string; title: string; culprit: string; permalink: string; level: string; status: string; platform: string; project: { id: string; name: string; slug: string }; type: string; metadata: { type?: string; value?: string; filename?: string; function?: string; }; count: string; userCount: number; firstSeen: string; lastSeen: string; stats?: Record; } export interface SentryEvent { eventID: string; title: string; message: string; dateCreated: string; context: Record; tags: { key: string; value: string }[]; user?: { id?: string; email?: string; username?: string }; } export interface SentryOrgStats { start: string; end: string; intervals: string[]; groups: { by: Record; totals: Record; series: Record; }[]; } // -- API functions -- export async function listOrganizations(): Promise { return apiGet("/organizations/"); } export async function listProjects(orgSlug?: string): Promise { const org = await getOrgSlug(orgSlug); return apiGet(`/organizations/${org}/projects/`); } export async function listIssues( projectSlug?: string, query?: string, statsPeriod?: string, orgSlug?: string, ): Promise { const org = await getOrgSlug(orgSlug); const params = new URLSearchParams(); if (query) params.set("query", query); if (statsPeriod) params.set("statsPeriod", statsPeriod); params.set("sort", "freq"); if (projectSlug) { return apiGet( `/projects/${org}/${projectSlug}/issues/?${params.toString()}`, ); } return apiGet( `/organizations/${org}/issues/?${params.toString()}`, ); } export async function getIssueEvents( issueId: string, orgSlug?: string, ): Promise { const org = await getOrgSlug(orgSlug); return apiGet( `/organizations/${org}/issues/${issueId}/events/`, ); } export async function getOrganizationStats( statsPeriod?: string, category?: string, orgSlug?: string, ): Promise { const org = await getOrgSlug(orgSlug); const params = new URLSearchParams(); params.set("field", "sum(quantity)"); if (statsPeriod) params.set("statsPeriod", statsPeriod); if (category) { params.set("category", category); } else { params.set("category", "error"); } params.set("groupBy", "outcome"); return apiGet( `/organizations/${org}/stats_v2/?${params.toString()}`, ); }