Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/api/src/sentry.ts
T

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;
}