106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
/**
|
|
* Sentry bootstrap for the API process.
|
|
*
|
|
* Exposes {@link initApiSentry} (called from `server.ts` as the very first
|
|
* statement) and {@link apiErrorReporter} (an `ErrorReporter` facade that
|
|
* `createErrorHandler` and any other code-path can use to forward unhandled
|
|
* errors to Sentry).
|
|
*
|
|
* When `SENTRY_DSN_API` is empty or unset every export becomes a no-op so
|
|
* local development and staging environments without a Sentry instance keep
|
|
* working unchanged.
|
|
*
|
|
* @see docs/improvement-plan/02-observability/sentry-wiring-api-worker.md
|
|
*/
|
|
import { createNoopErrorReporter, createSentryErrorReporter, type ErrorReporter } from "@hivegrid/error-reporter";
|
|
|
|
export interface ApiSentryConfig {
|
|
dsn: string | undefined;
|
|
environment: string | undefined;
|
|
release: string | undefined;
|
|
tracesSampleRate: number;
|
|
profilesSampleRate: number;
|
|
}
|
|
|
|
const DEFAULT_TRACES_SAMPLE_RATE_PROD = 0.1;
|
|
const DEFAULT_TRACES_SAMPLE_RATE_DEV = 1.0;
|
|
const DEFAULT_PROFILES_SAMPLE_RATE = 0.0;
|
|
|
|
export function parseSampleRate(raw: string | undefined, fallback: number): number {
|
|
if (raw === undefined || raw === null || raw === "") return fallback;
|
|
const parsed = Number(raw);
|
|
if (!Number.isFinite(parsed)) return fallback;
|
|
if (parsed < 0) return 0;
|
|
if (parsed > 1) return 1;
|
|
return parsed;
|
|
}
|
|
|
|
export function readApiSentryConfig(env: NodeJS.ProcessEnv = process.env): ApiSentryConfig {
|
|
const nodeEnv = env.NODE_ENV || "development";
|
|
const tracesDefault = nodeEnv === "production" ? DEFAULT_TRACES_SAMPLE_RATE_PROD : DEFAULT_TRACES_SAMPLE_RATE_DEV;
|
|
return {
|
|
dsn: env.SENTRY_DSN_API || undefined,
|
|
environment: env.SENTRY_ENVIRONMENT || nodeEnv || undefined,
|
|
release: env.SENTRY_RELEASE || env.APP_REVISION || undefined,
|
|
tracesSampleRate: parseSampleRate(env.SENTRY_TRACES_SAMPLE_RATE, tracesDefault),
|
|
profilesSampleRate: parseSampleRate(env.SENTRY_PROFILES_SAMPLE_RATE, DEFAULT_PROFILES_SAMPLE_RATE),
|
|
};
|
|
}
|
|
|
|
let cachedReporter: ErrorReporter | undefined;
|
|
let cachedConfig: ApiSentryConfig | undefined;
|
|
|
|
/**
|
|
* Idempotent. The first call builds the Sentry-backed reporter (or the
|
|
* no-op reporter when `SENTRY_DSN_API` is empty); subsequent calls return
|
|
* the cached instance. The Sentry SDK is loaded lazily through the adapter
|
|
* in `@hivegrid/error-reporter`, so unit tests that do not import this
|
|
* module never touch `@sentry/node` at all.
|
|
*/
|
|
export function initApiSentry(config: ApiSentryConfig = readApiSentryConfig()): ErrorReporter {
|
|
if (cachedReporter) {
|
|
return cachedReporter;
|
|
}
|
|
|
|
cachedConfig = config;
|
|
|
|
if (!config.dsn) {
|
|
cachedReporter = createNoopErrorReporter();
|
|
return cachedReporter;
|
|
}
|
|
|
|
cachedReporter = createSentryErrorReporter({
|
|
dsn: config.dsn,
|
|
environment: config.environment,
|
|
release: config.release,
|
|
tracesSampleRate: config.tracesSampleRate,
|
|
profilesSampleRate: config.profilesSampleRate,
|
|
maxBreadcrumbs: 50,
|
|
});
|
|
|
|
return cachedReporter;
|
|
}
|
|
|
|
/**
|
|
* Reporter facade. Lazy on first access so `apps/api/src/server.ts` controls
|
|
* when initialisation happens (and so unit tests that import `app.ts` without
|
|
* setting up env variables still get a no-op reporter).
|
|
*/
|
|
export const apiErrorReporter: ErrorReporter = {
|
|
report(event) {
|
|
const reporter = cachedReporter ?? initApiSentry();
|
|
return reporter.report(event);
|
|
},
|
|
};
|
|
|
|
/** Test-only: reset the cached reporter/config so multiple test cases can call init. */
|
|
export function __resetApiSentryForTests(): void {
|
|
cachedReporter = undefined;
|
|
cachedConfig = undefined;
|
|
}
|
|
|
|
/** Test-only: read the cached config (or undefined if not initialised). */
|
|
export function __getApiSentryConfigForTests(): ApiSentryConfig | undefined {
|
|
return cachedConfig;
|
|
}
|