Skip to content
← Public packages

@kentcdodds/sentry

Inspect Sentry organizations, projects, issues, and event summaries.

src/openapi-client.ts

123 lines · 4.5 KB · TypeScript
/**
 * Scaffolded Sentry OpenAPI client (dependency-free ESM).
 *
 * Source: openapi_client_scaffold against
 * https://raw.githubusercontent.com/getsentry/sentry-api-schema/main/openapi-derefed.json
 * Slugs: listorganizations, listorganizationprojects, listorganizationissues
 * Auth: bearerSecret sentryAuthToken
 *
 * Unscoped {{secret:sentryAuthToken}} resolves for this user-scoped secret
 * (verified HTTP 200); scoped form also works.
 */

export type QueryValue = string | number | boolean | null | undefined
export type QueryInput = Record<string, QueryValue | QueryValue[]>

const API_BASE_URL = 'https://sentry.io'
const SENTRY_AUTH_HEADER = 'Bearer {{secret:sentryAuthToken}}'

export function buildUrl(pathTemplate: string, params: Record<string, unknown> = {}): string {
  return API_BASE_URL + pathTemplate.replace(/\{([^}]+)\}/g, (_match, name: string) => {
    const value = params[name]
    if (value === undefined || value === null) {
      throw new Error(`Missing required path parameter: ${name}`)
    }
    return encodeURIComponent(String(value))
  })
}

export function appendQuery(url: string, query: QueryInput = {}): string {
  const search = new URLSearchParams()
  for (const [key, value] of Object.entries(query)) {
    if (value === undefined || value === null || value === '') continue
    if (Array.isArray(value)) {
      for (const item of value) {
        if (item === undefined || item === null || item === '') continue
        search.append(key, String(item))
      }
      continue
    }
    search.append(key, String(value))
  }
  const qs = search.toString()
  return qs ? `${url}?${qs}` : url
}

function mergeHeaders(
  userHeaders: Record<string, string> | undefined,
  authHeaders: Record<string, string>,
): Record<string, string> {
  const merged: Record<string, string> = { ...(userHeaders ?? {}) }
  for (const [key, value] of Object.entries(authHeaders)) {
    const lower = key.toLowerCase()
    for (const existing of Object.keys(merged)) {
      if (existing.toLowerCase() === lower) delete merged[existing]
    }
    merged[key] = value
  }
  return merged
}

function authHeaders(): Record<string, string> {
  return { Authorization: SENTRY_AUTH_HEADER }
}

export type ScaffoldInput = {
  params?: Record<string, unknown>
  query?: QueryInput
  headers?: Record<string, string>
  body?: unknown
}

/** GET /api/0/organizations/ — List Your Organizations */
export async function listorganizations(input: ScaffoldInput = {}): Promise<Response> {
  const params = input.params ?? {}
  const url = appendQuery(buildUrl('/api/0/organizations/', params), input.query)
  return fetch(url, { method: 'GET', headers: mergeHeaders(input.headers, authHeaders()) })
}

/** GET /api/0/organizations/{organization_id_or_slug}/projects/ */
export async function listorganizationprojects(input: ScaffoldInput = {}): Promise<Response> {
  const params = input.params ?? {}
  if (params.organization_id_or_slug === undefined || params.organization_id_or_slug === null) {
    throw new Error('Missing required path parameter: organization_id_or_slug')
  }
  const url = appendQuery(
    buildUrl('/api/0/organizations/{organization_id_or_slug}/projects/', params),
    input.query,
  )
  return fetch(url, { method: 'GET', headers: mergeHeaders(input.headers, authHeaders()) })
}

/** GET /api/0/organizations/{organization_id_or_slug}/issues/ */
export async function listorganizationissues(input: ScaffoldInput = {}): Promise<Response> {
  const params = input.params ?? {}
  if (params.organization_id_or_slug === undefined || params.organization_id_or_slug === null) {
    throw new Error('Missing required path parameter: organization_id_or_slug')
  }
  const url = appendQuery(
    buildUrl('/api/0/organizations/{organization_id_or_slug}/issues/', params),
    input.query,
  )
  return fetch(url, { method: 'GET', headers: mergeHeaders(input.headers, authHeaders()) })
}

/**
 * Minimal fetch for endpoints absent from the public OpenAPI inventory
 * (e.g. /api/0/issues/{id}/events/latest/). Path is relative to /api/0.
 */
export async function rawSentryRequest(
  path: string,
  options: { query?: QueryInput; init?: RequestInit } = {},
): Promise<Response> {
  const normalizedPath = path.startsWith('/') ? path : '/' + path
  const url = appendQuery(`${API_BASE_URL}/api/0${normalizedPath}`, options.query)
  const headers = mergeHeaders(
    {
      accept: 'application/json',
      ...(options.init?.headers as Record<string, string> | undefined),
    },
    authHeaders(),
  )
  return fetch(url, { ...options.init, headers })
}