/**
 * API request instrumentation that powers the Dashboard "Delivery API"
 * tiles (request volume / error rate / avg latency / p95).
 *
 * ## Why per-minute aggregated buckets and not per-request rows
 *
 * A single 24-hour window at 100 req/s on 20 routes is ~17M raw rows,
 * but the same window aggregated to (minute × route × status-bucket)
 * is ≤ 1440 × 20 × 5 = 144k rows — and in practice far less because
 * most route/minute combinations have zero traffic. The reader
 * (`/stats/api-delivery`) folds the last 1440 minute buckets into
 * four scalars; the writer does one atomic UPSERT per request via
 * raw SQL.
 *
 * ## Why p95 via histogram and not from raw samples
 *
 * Storing a histogram per bucket gives us percentile estimates with
 * ~10% error vs ground truth while keeping the table tiny. The
 * `LATENCY_BUCKETS` boundary list is log-scale (powers of 2 in
 * milliseconds) so it covers ~1 ms heartbeats through multi-second
 * outliers in 12 columns. p95 falls out as a `findIndex` once the
 * histogram is merged across all minute buckets in the window.
 *
 * ## Why fire-and-forget and not awaited
 *
 * Metric recording MUST NOT slow the user-facing response. The
 * `record()` call dispatches the UPSERT to a `void`-discarded promise
 * so a slow DB round-trip never blocks the request that produced
 * the metric. Errors are swallowed at the source — losing a metric
 * sample is strictly preferable to surfacing a metric-pipeline error
 * to the user.
 *
 * ## Why we self-exempt the dashboard polling routes
 *
 * The dashboard refetches `/stats` and `/stats/api-delivery` on a
 * timer. If we counted those requests they'd dominate the volume
 * tile and inflate the error rate (a logged-out tab would hit
 * `/stats` and get 401 repeatedly). The route-normaliser excludes
 * them explicitly — see `shouldRecord`.
 */
/**
 * Upper bound (exclusive) of each histogram bucket, in milliseconds.
 * The last bucket is `>= 1024 ms` (no upper bound).
 *
 * Indexed:
 *   0: <1ms     1: 1-2ms     2: 2-4ms     3: 4-8ms
 *   4: 8-16ms   5: 16-32ms   6: 32-64ms   7: 64-128ms
 *   8: 128-256  9: 256-512  10: 512-1024 11: >=1024
 */
export declare const LATENCY_BUCKETS: readonly [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024];
export declare const HISTOGRAM_LENGTH: number;
export type StatusBucket = '2xx' | '3xx' | '4xx' | '5xx' | 'other';
export interface ApiMetricSample {
    /** Request path, CMS-relative — e.g. `/collections/posts/abc123`. */
    pathname: string;
    status: number;
    /** Response time in milliseconds. May be a float. */
    durationMs: number;
}
/**
 * Bucket a status code into one of 5 families. We keep the 1xx family
 * out of the public type because no CMS route ever responds with one;
 * if it ever happens, the `other` bucket catches it instead of
 * silently being lost.
 */
export declare function statusBucketOf(status: number): StatusBucket;
/**
 * Return the index (0..HISTOGRAM_LENGTH-1) of the histogram bucket
 * that this latency falls into. NaN / negative values fall in bucket
 * 0 (treated as "<1ms") so a metric write never crashes on a bad
 * input.
 */
export declare function histogramBucketIndex(durationMs: number): number;
/**
 * Build a fresh histogram array with a single sample placed in the
 * correct bucket. Used to produce the JSON payload for an INSERT.
 */
export declare function makeHistogramFor(durationMs: number): number[];
/**
 * Element-wise add two histograms. Tolerant of either operand being
 * missing or shorter than `HISTOGRAM_LENGTH` — defends against schema
 * mismatches (e.g. an old row written before a bucket boundary change).
 */
export declare function mergeHistograms(a: number[] | null | undefined, b: number[]): number[];
/**
 * Snap a Date to the start of its UTC minute. Returns a new Date —
 * never mutates the input.
 */
export declare function bucketStartFor(now?: Date | number): Date;
/**
 * Estimate percentile-N from a merged histogram. Returns the upper
 * bound of the bucket that contains the requested percentile —
 * which is the right "user-visible" answer (we are happy to say
 * "p95 < 256ms" rather than guess the exact value inside the
 * 128-256 bucket).
 *
 * For the overflow bucket (no upper bound), we report
 * `LATENCY_BUCKETS[last]` as a lower-bound estimate; callers
 * displaying this should annotate with a "≥" prefix if they want
 * to expose the open-ended nature.
 */
export declare function estimatePercentile(histogram: number[], percentile: number): number;
/**
 * Normalise a request path to bound metric-row cardinality.
 *
 *   /collections/posts/abc123        →  /collections/posts/:id
 *   /globals/site-settings           →  /globals/:slug
 *   /collections/posts/abc123/restore→  /collections/posts/:id/restore
 *   /unknown/long/path/that/leaks    →  /other
 *
 * The "/other" bucket catches anything we don't explicitly know
 * about so a single hostile client with random URLs can't blow up
 * the table.
 */
export declare function normalizeRoute(pathname: string): string;
/**
 * Routes that the dashboard polls itself, plus webhooks the cron
 * fires. Counting these would either (a) cause feedback loops
 * inflating the volume tile or (b) report cron-only traffic the
 * editor never produces. The instrumentation skips them.
 */
export declare function shouldRecord(normalizedRoute: string): boolean;
/**
 * Record one API request to the metrics table. **Fire-and-forget** —
 * callers MUST NOT `await` this. Errors are swallowed; the next
 * request retries, and 1-minute aggregates are robust to occasional
 * write loss.
 *
 * Writes a single SQL UPSERT so two concurrent requests landing in
 * the same bucket interleave cleanly via Postgres's MVCC + the
 * unique-on-conflict path. The histogram column is merged
 * element-wise inside SQL, so we never read-modify-write from JS.
 */
export declare function recordApiRequest(sample: ApiMetricSample): void;
//# sourceMappingURL=api-metrics.d.ts.map