import { kody } from 'kody:runtime'
import {
DEFAULT_SECRET_NAME,
oauthConnectUrl,
resolveIntegrationName,
resolveSecretName,
secretSetupUrl,
} from './auth.ts'
import { listOrganizations } from './client.ts'
import { DEFAULT_SENTRY_HOST, runHostHelperSelfCheck } from './host.ts'
import type { SentryClientOptions } from './types.ts'
export type SentrySmokeTestResult = {
ok: boolean
selfCheck: { ok: true; checks: number }
live: boolean
host: string
secretPresent: boolean
integration?: string
secretName: string
organizations: Array<{ slug: string; name?: string }>
setup: {
nextSteps: Array<string>
}
}
function secretEntries(result: unknown): Array<{ name?: string; scope?: string }> {
if (result && typeof result === 'object' && Array.isArray((result as { secrets?: unknown }).secrets)) {
return (result as { secrets: Array<{ name?: string; scope?: string }> }).secrets
}
return Array.isArray(result) ? result : []
}
async function hasUserSecret(name: string): Promise<boolean> {
const listed = await kody.secret_list({ scope: 'user' })
for (const entry of secretEntries(listed)) {
if (entry?.name === name && (entry.scope === 'user' || !entry.scope)) {
return true
}
}
return false
}
/**
* Local host-normalization checks, plus an optional live organization list.
*
* Without `sentryAuthToken` (or the selected `secretName` / `integration`) this
* still returns `{ ok: true, live: false }` and the prefilled secret URL so the
* package is forkable before anyone connects.
*
* @example
* import smokeTest from 'kody:@kody/sentry/smoke-test'
* const result = await smokeTest()
*/
export async function smokeTest(params: SentryClientOptions = {}): Promise<SentrySmokeTestResult> {
const selfCheck = runHostHelperSelfCheck()
const host = params.host?.trim() || DEFAULT_SENTRY_HOST
const integration = resolveIntegrationName(params)
const secretName = resolveSecretName(params)
const nextSteps: Array<string> = []
if (integration) {
const organizations = await listOrganizations(params)
return {
ok: true,
selfCheck,
live: true,
host,
secretPresent: true,
integration,
secretName,
organizations: organizations.map((org) => ({
slug: org.slug,
name: typeof org.name === 'string' ? org.name : undefined,
})),
setup: { nextSteps: [] },
}
}
const secretPresent = await hasUserSecret(secretName)
if (!secretPresent) {
nextSteps.push(
'Save a Sentry user or internal-integration auth token as ' + secretName + '.',
)
nextSteps.push(secretSetupUrl(secretName))
nextSteps.push(
'Approve hosts sentry.io, us.sentry.io, and de.sentry.io (plus any self-hosted host).',
)
nextSteps.push(
'Optional BYO OAuth (only if you already have a Sentry OAuth app): ' + oauthConnectUrl(),
)
return {
ok: true,
selfCheck,
live: false,
host,
secretPresent,
secretName,
organizations: [],
setup: { nextSteps },
}
}
const organizations = await listOrganizations(params)
return {
ok: true,
selfCheck,
live: true,
host,
secretPresent,
secretName,
organizations: organizations.map((org) => ({
slug: org.slug,
name: typeof org.name === 'string' ? org.name : undefined,
})),
setup: { nextSteps: [] },
}
}
/** Default export: smoke test. */
export default smokeTest