81 lines
3.2 KiB
TypeScript
81 lines
3.2 KiB
TypeScript
#!/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<void> {
|
|
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> | void) => Promise<void> | void;
|
|
default?: {
|
|
startSpan?: (args: { op: string; name: string }, cb: () => Promise<void> | void) => Promise<void> | 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);
|
|
});
|