Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/sentry

src/smoke-test.ts

121 lines · 3.2 KB · TypeScript
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