#!/usr/bin/env tsx /** * Sentry smoke test for the API process. * * Triggers a fake captureException + a fake `Sentry.startSpan` transaction * so an operator can verify end-to-end that: * - the API DSN is wired, * - `beforeSend` sanitises PII, * - the matching Sentry project receives both an Issue and a Transaction. * * Run from the repo root with one of: * pnpm --filter @toolctrl/api exec tsx scripts/smoke-sentry.ts * SENTRY_DSN_API=... pnpm --filter @toolctrl/api exec tsx scripts/smoke-sentry.ts * * Exits 0 on a clean run, 1 when the SDK fails to load. A no-DSN run is * intentionally a successful no-op so CI can call it unconditionally. * * @see docs/improvement-plan/02-observability/sentry-wiring-api-worker.md */ import { initApiSentry, readApiSentryConfig } from "../src/sentry.js"; async function main(): Promise { const config = readApiSentryConfig(); if (!config.dsn) { // eslint-disable-next-line no-console console.warn("[smoke-sentry] SENTRY_DSN_API is not set; nothing to do."); return; } const reporter = initApiSentry(config); // 1) Fake captureException — exercises beforeSend + sanitisation. await reporter.report({ component: "api", operation: "smoke.exception", severity: "error", error: new Error(`smoke-sentry: fake exception at ${new Date().toISOString()}`), // Include obvious PII fields so the operator can confirm redaction in the // Sentry UI (`token` / `password` should render as [Redacted]). token: "fake-token-must-be-redacted", password: "fake-password-must-be-redacted", }); // 2) Fake transaction — exercises @sentry/node startSpan. try { const moduleName = "@sentry/node"; const sentry = (await import(/* @vite-ignore */ moduleName)) as { startSpan?: (args: { op: string; name: string }, cb: () => Promise | void) => Promise | void; default?: { startSpan?: (args: { op: string; name: string }, cb: () => Promise | void) => Promise | void; }; }; const startSpan = sentry.startSpan ?? sentry.default?.startSpan; if (typeof startSpan === "function") { await startSpan({ op: "test", name: "smoke-sentry/api" }, async () => { await new Promise((resolve) => setTimeout(resolve, 25)); }); } else { // eslint-disable-next-line no-console console.warn("[smoke-sentry] @sentry/node loaded but startSpan is unavailable; transaction skipped."); } } catch (cause) { // eslint-disable-next-line no-console console.error("[smoke-sentry] failed to load @sentry/node for the transaction smoke:", cause); process.exitCode = 1; } // Give the SDK enough time to flush. Sentry's `close()` would be more // precise but pulling the type via dynamic import is more trouble than it // is worth for a manual smoke script. await new Promise((resolve) => setTimeout(resolve, 1500)); // eslint-disable-next-line no-console console.log("[smoke-sentry] done — check the Sentry UI (Issues + Performance) for boilerplate-api."); } main().catch((cause) => { // eslint-disable-next-line no-console console.error("[smoke-sentry] unexpected failure:", cause); process.exit(1); });