UNPKG

cyrus-ai

Version:

AI-powered Linear issue automation using Claude

130 lines 5.46 kB
import { NoopErrorReporter, setGlobalErrorTags, } from "cyrus-core"; import { SentryErrorReporter } from "./SentryErrorReporter.js"; import { scrubSentryEvent, scrubSentryLog } from "./sentryScrubber.js"; /** * Default DSN baked into release builds. Empty until an admin creates the * `ceedar/cyrus-cli` Sentry project and pastes the DSN here. Sentry DSNs are * safe to publish — they only authorise event ingestion. * * End users may override this with the `CYRUS_SENTRY_DSN` env var, or disable * reporting entirely with `CYRUS_SENTRY_DISABLED=1`. */ export const DEFAULT_SENTRY_DSN = "https://4a343e39f7439cb5669604657fca148e@o4509685010399232.ingest.us.sentry.io/4511293576839168"; /** * Build the application's {@link ErrorReporter}. * * Order of resolution: * 1. If `CYRUS_SENTRY_DISABLED` is truthy → noop. * 2. If `CYRUS_TEAM_ID` is unset → noop. Both Issues and Logs require a * tenant tag so we can slice/filter per Cyrus install in Sentry; without * it we send nothing rather than emit untenanted noise. * 3. Else if a DSN is available (env var or compiled default) → Sentry. * 4. Else → noop. * * Initialise this as early as possible during process startup so that * exceptions thrown by subsequent imports/bootstrap are captured. */ export function createErrorReporter(params = {}) { const env = params.env ?? process.env; if (isTruthyEnv(env.CYRUS_SENTRY_DISABLED)) { return new NoopErrorReporter(); } const tags = buildInitialTags(env); // CYRUS_TEAM_ID is the single gate for *both* Issues and Logs — installs // without tenant tagging stay silent so the team's Sentry org isn't // flooded with untenanted self-hosted noise we can't slice. if (!tags?.team_id) { return new NoopErrorReporter(); } const dsn = env.CYRUS_SENTRY_DSN?.trim() || DEFAULT_SENTRY_DSN; if (!dsn) { return new NoopErrorReporter(); } // Mirror the tag set into the process-wide registry so Logger.error // forwarding (which builds its own per-event tag map) includes them too, // not just events emitted directly via the Sentry SDK's initialScope. setGlobalErrorTags(tags); const environment = env.CYRUS_SENTRY_ENVIRONMENT?.trim() || "production"; return new SentryErrorReporter({ dsn, release: params.release, environment, // Sentry SDK debug output is unrelated to app log level — gating it on // CYRUS_LOG_LEVEL=DEBUG floods stdout with OpenTelemetry tracing // inheritance / client-report flush messages that belong to the SDK, // not Cyrus. Use a dedicated opt-in env var. debug: isTruthyEnv(env.CYRUS_SENTRY_DEBUG), tags, structuredContext: buildStructuredContext({ env, environment, release: params.release, tags, }), sampleRate: parseSampleRate(env.CYRUS_SENTRY_SAMPLE_RATE), // Always scrub. Cyrus' logger.error sites pass arbitrary args (request // bodies, configs, headers) that may carry tokens; we cannot trust call // sites to redact, so we filter on the way out. beforeSend: scrubSentryEvent, // Logs use a separate ingestion path from Issues — `beforeSend` does // not run on them, so we register a dedicated hook with the same scrub // rules to keep both paths symmetric. beforeSendLog: scrubSentryLog, }); } /** * Build the structured `cyrus` context block attached to every event. This is * the structured-logging counterpart to {@link buildInitialTags}: tags get * the indexed/searchable subset (team_id), the context block carries the * richer typed fields that show up grouped in the Sentry UI. */ function buildStructuredContext(input) { const ctx = { environment: input.environment, }; if (input.release) ctx.release = input.release; if (input.tags?.team_id) ctx.team_id = input.tags.team_id; const linearWorkspace = input.env.CYRUS_LINEAR_WORKSPACE?.trim(); if (linearWorkspace) ctx.linear_workspace = linearWorkspace; const deployment = input.env.CYRUS_DEPLOYMENT_ID?.trim(); if (deployment) ctx.deployment_id = deployment; return Object.keys(ctx).length > 1 || ctx.team_id ? ctx : undefined; } /** * Parse a sample rate from env. Returns undefined for malformed/empty values * so the SentryErrorReporter default (1.0) applies. */ function parseSampleRate(value) { if (!value) return undefined; const n = Number.parseFloat(value); if (!Number.isFinite(n) || n < 0 || n > 1) return undefined; return n; } /** * Build the global tag set applied to every Sentry event. Currently picks up * `CYRUS_TEAM_ID` and exposes it as the `team_id` tag so events can be * filtered per Cyrus tenant in Sentry. * * Add additional process-wide tags here rather than at capture sites — keeps * call sites free of cross-cutting concerns. */ function buildInitialTags(env) { const tags = {}; const teamId = env.CYRUS_TEAM_ID?.trim(); if (teamId) tags.team_id = teamId; return Object.keys(tags).length > 0 ? tags : undefined; } function isTruthyEnv(value) { if (!value) return false; const v = value.trim().toLowerCase(); return v === "1" || v === "true" || v === "yes" || v === "on"; } //# sourceMappingURL=createErrorReporter.js.map