import type { AlertChannel } from './alert-channel.js';
/**
 * A single alerting rule. Each rule is evaluated on every tick of the alerter,
 * EXCEPT `new-exception` which evaluates per-flush over just-stored exception
 * entries (so a brand-new error family pages you within a flush interval, not a
 * full evaluation interval).
 *
 * - `exception-rate`    — fires when `>= threshold` exception entries were
 *                         recorded in the trailing `window`.
 * - `slow-request-rate` — fires when `>= count` request entries slower than
 *                         `thresholdMs` were recorded in the trailing `window`.
 * - `dropped-entries`   — fires when the Recorder's cumulative `droppedCount`
 *                         grew by `>= threshold` since the previous evaluation
 *                         (a delta, not an absolute — so a once-busy host that
 *                         dropped a burst at boot doesn't keep re-firing).
 * - `new-exception`     — fires the FIRST time an exception's `familyHash` is
 *                         seen within `window` (a genuinely NEW error family).
 *                         Dedup uses the shared {@link StorageProvider} when it
 *                         implements `markFamilySeen` (so a family pages ONCE
 *                         across a multi-replica deployment), falling back to an
 *                         in-memory per-replica seen-map otherwise.
 * - `every-exception`   — fires for EVERY exception (server + browser-reported
 *                         `client_exception`), not just brand-new families —
 *                         parity with a "notify on every error" setup. Still
 *                         rate-limited by the shared `cooldown` PER FAMILY, so a
 *                         hot loop of the same error re-pages once per cooldown
 *                         rather than on every occurrence; set `cooldown: '0s'`
 *                         for a truly uncollapsed stream. The optional `window`
 *                         is used only to count occurrences shown on the alert
 *                         (default `'1h'`); it does NOT gate firing.
 * - `metric-threshold`  — fires when a COMPUTED metric over the trailing
 *                         `window` crosses `threshold` in the `comparator`
 *                         direction. Unlike the rate rules (which count events),
 *                         this evaluates the SAME aggregates the dashboard shows
 *                         — e.g. request p95 latency, or cache hit-rate — so you
 *                         can page on "p95 > 800ms" or "hit-rate < 0.8".
 */
export type AlertRule = {
    type: 'exception-rate';
    window: string;
    threshold: number;
} | {
    type: 'slow-request-rate';
    window: string;
    thresholdMs: number;
    count: number;
} | {
    type: 'dropped-entries';
    threshold: number;
} | {
    type: 'new-exception';
    window: string;
} | {
    type: 'every-exception';
    window?: string;
} | {
    type: 'metric-threshold';
    /** The computed metric to evaluate (see {@link AlertMetric}). */
    metric: AlertMetric;
    /** Trailing window to aggregate over (e.g. `'5m'`). */
    window: string;
    /** `gte` fires when value >= threshold; `lte` when value <= threshold. */
    comparator: 'gte' | 'lte';
    /** The threshold the metric is compared against. */
    threshold: number;
    /**
     * Minimum samples in the window before the rule can fire. Guards against a
     * single slow request tripping a p95 page on a quiet host. Default 1.
     */
    minSamples?: number;
};
/**
 * The metrics a `metric-threshold` rule can evaluate. Each is derived from the
 * SAME windowed aggregation the stats/pulse views use, so an alert means exactly
 * what the dashboard shows. Latency metrics are in ms; `cache-hit-rate` is a
 * ratio in [0, 1].
 */
export type AlertMetric = 'request-p95-ms' | 'request-p99-ms' | 'query-p95-ms' | 'query-p99-ms' | 'cache-hit-rate';
/**
 * A coarse geo location resolved from a client IP, attached to an exception alert
 * when a {@link AlertsOptions.geoLookup} hook is configured. Every field is
 * optional — a partial result (e.g. country only) still renders. Deliberately
 * dependency-light: the LIB ships no geo database or HTTP client; the host owns
 * the lookup (and its caching/rate-limiting) and returns this shape.
 */
export interface AlertGeoLocation {
    city?: string;
    region?: string;
    country?: string;
    /** ISO 3166-1 alpha-2 (e.g. `US`), used to render a flag emoji. */
    countryCode?: string;
}
/**
 * Host-supplied resolver from a client IP to a coarse {@link AlertGeoLocation}.
 * Called ONLY when an exception alert actually fires and carries a `clientIp`, so
 * the common no-fire path pays nothing. May be sync or async; returning `null`
 * (or throwing — swallowed) simply omits the Location field. The lib never caches
 * or rate-limits it — do that in the hook if the provider needs it.
 */
export type GeoLookup = (ip: string) => AlertGeoLocation | null | Promise<AlertGeoLocation | null>;
/**
 * Pluggable alerting (v2). When set, {@link TelescopeAlerter} evaluates `rules`
 * and fans each fired alert out to EVERY configured channel concurrently. A
 * configured `alerts` with no destination (neither `channels` nor the legacy
 * `webhookUrl`) or empty `rules` is a fail-closed boot error.
 */
export interface AlertsOptions {
    /**
     * Optional IP→geo resolver. When set, a firing exception alert that carries a
     * `clientIp` is enriched with a coarse {@link AlertGeoLocation} (rendered as a
     * "Location" field by channels that support it). Kept out of the lib core so
     * telescope ships no geo dependency — see {@link GeoLookup}.
     */
    geoLookup?: GeoLookup;
    /**
     * Delivery destinations. Each fired alert is sent to every channel
     * concurrently; one channel failing never blocks the others. Use the factory
     * helpers: `slackChannel(url)`, `webhookChannel(url)`, `customChannel(fn)`.
     * Optional ONLY for backward compatibility with `webhookUrl` — supply at least
     * one of `channels` / `webhookUrl`.
     */
    channels?: AlertChannel[];
    /**
     * Legacy single raw-JSON webhook (v1). Still accepted: internally rewritten
     * into a `webhookChannel(webhookUrl)` and appended to `channels`. Prefer
     * `channels` for new configs.
     */
    webhookUrl?: string;
    /**
     * The host's EXTERNAL Telescope dashboard URL (e.g.
     * `https://telescope.example.com/telescope/`). When set, channels that support
     * deep links (Slack) build a link straight to the offending entry, e.g.
     * `${dashboardUrl}#/entries/exception/${id}`. Optional; without it the Slack
     * message simply omits the "Open in Telescope" button.
     */
    dashboardUrl?: string;
    /**
     * Evaluation cadence as a duration string (e.g. `'1m'`). Default `'1m'`.
     * Resolved to ms via `durationToMs`. (Interval rules only; `new-exception`
     * evaluates per-flush.)
     */
    every?: string;
    /** Re-notify suppression per rule, as a duration string. Default `'15m'`. */
    cooldown?: string;
    /** Rules to evaluate. Must be non-empty when `alerts` is set. */
    rules: AlertRule[];
}
/** Boot-resolved, validated alerting config (durations normalized to ms). */
export interface ResolvedAlerts {
    /** All destinations, with any legacy `webhookUrl` already folded in. */
    channels: AlertChannel[];
    /** External dashboard URL for deep links, or `null` when unset. */
    dashboardUrl: string | null;
    intervalMs: number;
    cooldownMs: number;
    rules: AlertRule[];
    /** Host IP→geo resolver, or `null` when unconfigured. */
    geoLookup: GeoLookup | null;
}
/**
 * Rich exception context attached to a `new-exception` alert. Pulled from the
 * exception entry AND its sibling request entry in the SAME batch (queried by
 * `batchId` only when the rule actually fires, so the common no-fire path stays
 * a single in-memory map lookup). Absent on rate-rule alerts.
 */
export interface ExceptionAlertContext {
    /** Stable family hash that was first-seen this window. */
    familyHash: string;
    /** Exception class name (e.g. `TypeError`). */
    class: string;
    /** Exception message. */
    message: string;
    /** Truncated stack (first frames), or `null`. */
    stack: string | null;
    /**
     * Where the error happened: the sibling request's route/uri for a server
     * exception, or the browser page `url` for a `client_exception`. `null` when
     * neither is available.
     */
    route: string | null;
    /** Request method, or `null` (always `null` for a client_exception). */
    method: string | null;
    /**
     * User-agent string. For a `client_exception` it's the reporting browser's UA;
     * for a server exception it's the sibling request's `user-agent` header (when
     * captured). `null` when unavailable.
     */
    userAgent: string | null;
    /**
     * `Referer` header of the originating request (server exception) — the page a
     * user came from. `null` when absent or for a `client_exception` (the browser
     * report carries its own page `url` in `route`, not a referer).
     */
    referer: string | null;
    /**
     * React component stack from an error boundary — present ONLY for a
     * `client_exception` that supplied one. `null` otherwise.
     */
    componentStack: string | null;
    /**
     * Host-defined free-form debugging bag from a `client_exception` (`extra`), or
     * `null`. Redacted/bounded at record time like any other content.
     */
    extra: Record<string, unknown> | null;
    /** True when this alert is a browser-reported `client_exception`. */
    client: boolean;
    /**
     * Originating client IP, or `null` when unknown. For a `client_exception` it's
     * the browser IP the ingestion controller filled in server-side (`clientIp`);
     * for a server exception it's the sibling request entry's `ip` (`request.ip` /
     * the first `x-forwarded-for` hop). Never sourced from an untrusted body.
     */
    clientIp: string | null;
    /**
     * Coarse geo location resolved from {@link clientIp}, or `null`. Populated only
     * when an {@link AlertsOptions.geoLookup} hook is configured AND the alert has a
     * `clientIp`; otherwise `null`.
     */
    geo: AlertGeoLocation | null;
    /** Response status code, or `null`. */
    statusCode: number | null;
    /** Request duration (ms), or `null`. */
    durationMs: number | null;
    /** Authenticated user id from a `user:<id>` tag, or `null`. */
    user: string | null;
    /** Times this family was seen in the window (>= 1; 1 on first-occurrence). */
    occurrences: number;
    /**
     * True when this is the family's first occurrence in the window (`occurrences
     * === 1`) — a brand-new error rather than a recurrence. Lets channels badge a
     * new error distinctly from a recurring one.
     */
    isNew: boolean;
    /** Exception entry id (for the dashboard deep link). */
    entryId: string;
    /** Batch id that ties the exception to its request entry. */
    batchId: string;
}
/**
 * Shape delivered to channels when a rule fires. BACKWARD COMPATIBLE with the v1
 * raw-webhook JSON: every v1 field (`rule`, `value`, `threshold`, `firedAt`,
 * `instanceId`) is unchanged; new fields are purely additive and optional.
 */
export interface AlertPayload {
    rule: AlertRule;
    /** The measured value that crossed the threshold. */
    value: number;
    /** The rule's threshold (`threshold`/`count`, or `1` for `new-exception`). */
    threshold: number;
    /** ISO-8601 fire time. */
    firedAt: string;
    /** The reporting instance (`config.instanceId`). */
    instanceId: string;
    /** Rich context for `new-exception` alerts; absent for rate rules. */
    exception?: ExceptionAlertContext;
    /** External dashboard URL when configured (lets channels build deep links). */
    dashboardUrl?: string;
    /**
     * AI-generated probable-cause markdown for a `new-exception` alert, attached
     * when `ai` is configured in `'auto'` mode AND the diagnosis finished within
     * the alert's short grace window. Absent when AI is off, the diagnosis was
     * still running at dispatch time, or it failed — the alert always fires
     * regardless, AI is purely additive. Channels that render it (Slack) append a
     * "Probable cause (AI)" section.
     */
    diagnosis?: string;
}
//# sourceMappingURL=alert-rule.d.ts.map